3 # Copyright 2019 Koha Development team
5 # This file is part of Koha
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.
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.
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>.
22 use Test::More tests => 29;
27 use Koha::CirculationRules;
29 use Koha::DateUtils qw(dt_from_string);
30 use Koha::ArticleRequests;
32 use Koha::Patron::Relationships;
33 use C4::Circulation qw( AddIssue AddReturn );
35 use t::lib::TestBuilder;
38 my $schema = Koha::Database->new->schema;
39 my $builder = t::lib::TestBuilder->new;
41 subtest 'Accessor tests' => sub {
43 $schema->storage->txn_begin;
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" );
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" );
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" );
61 our $patron = Koha::Patron->new(
63 borrowernumber => '12345',
64 cardnumber => '1234567890',
65 surname => 'mySurname',
66 firstname => 'myFirstname',
68 othernames => 'myOthernames',
70 streetnumber => '100',
72 address => 'my personal address',
73 address2 => 'my adress2',
78 email => 'mySurname.myFirstname@email.com',
79 phone => '0402872934',
80 mobile => '0627884632',
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',
89 B_state => 'myB_state',
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',
101 debarred => '2015-04-19',
102 debarredcomment => 'You are debarred',
103 borrowernotes => 'borrowernotes',
105 password => 'hfkurhfe976634èj!',
108 opacnote => 'myOpacnote',
109 contactnote => 'myContactnote',
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',
126 subtest 'Accessor tests after new' => sub {
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' );
178 $patron->altcontactfirstname, 'myAltcontactfirstname',
179 'altcontactfirstname accessor returns correct value'
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' );
193 subtest 'Accessor tests after set' => sub {
198 borrowernumber => '12346',
199 cardnumber => '1234567891',
200 surname => 'SmySurname',
201 firstname => 'SmyFirstname',
203 othernames => 'SmyOthernames',
205 streetnumber => '200',
207 address => 'Smy personal address',
208 address2 => 'Smy adress2',
213 email => 'SmySurname.myFirstname@email.com',
214 phone => '0402872935',
215 mobile => '0627884633',
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',
236 debarred => '2016-04-19',
237 debarredcomment => 'You are still debarred',
238 borrowernotes => 'Sborrowernotes',
240 password => 'zerzerzer#',
243 opacnote => 'SmyOpacnote',
244 contactnote => 'SmyContactnote',
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',
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' );
324 subtest 'is_active' => sub {
326 $schema->storage->txn_begin;
328 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
329 throws_ok { $patron->is_active } 'Koha::Exceptions::MissingParameter', 'Called without params';
332 $patron->dateexpiry( dt_from_string->subtract( days => 1 ) )->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' );
337 # Change enrolled date now
338 $patron->dateenrolled('2020-01-01')->store;
340 # Check lastseen, test days parameter
341 t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', 1 );
342 $patron->track_login;
343 is( $patron->is_active( { days => 1 } ), 1, 'Just logged in' );
344 my $ago = dt_from_string->subtract( days => 2 );
345 $patron->lastseen($ago)->store;
346 is( $patron->is_active( { days => 1 } ), 0, 'Not active since yesterday' );
347 is( $patron->is_active( { days => 3 } ), 1, 'Active within last 3 days' );
348 t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', 0 );
349 is( $patron->is_active( { days => 3 } ), 0, 'Pref disabled' );
351 # Look at holds, test with weeks
352 $ago = dt_from_string->subtract( weeks => 2 );
353 my $hold = $builder->build_object(
355 class => 'Koha::Holds',
356 value => { borrowernumber => $patron->id, timestamp => $ago },
359 is( $patron->is_active( { weeks => 1 } ), 0, 'No holds in 1 weeks' );
360 is( $patron->is_active( { weeks => 3 } ), 1, 'Hold in last 3 weeks' );
362 my $old_hold = $builder->build_object(
364 class => 'Koha::Old::Holds',
365 value => { borrowernumber => $patron->id, timestamp => $ago },
368 is( $patron->is_active( { weeks => 1 } ), 0, 'No old holds in 1 weeks' );
369 is( $patron->is_active( { weeks => 3 } ), 1, 'Old hold in last 3 weeks' );
372 # Look at checkouts, test with months
373 $ago = dt_from_string->subtract( months => 2 );
374 my $checkout = $builder->build_object(
376 class => 'Koha::Checkouts',
377 value => { borrowernumber => $patron->id, timestamp => $ago },
380 is( $patron->is_active( { months => 1 } ), 0, 'No checkouts in 1 months' );
381 is( $patron->is_active( { months => 3 } ), 1, 'Checkout in last 3 months' );
383 my $old_checkout = $builder->build_object(
385 class => 'Koha::Old::Checkouts',
386 value => { borrowernumber => $patron->id, timestamp => $ago },
389 is( $patron->is_active( { months => 1 } ), 0, 'No old checkouts in 1 months' );
390 is( $patron->is_active( { months => 3 } ), 1, 'Old checkout in last 3 months' );
391 $old_checkout->delete;
393 # Look at article_requests, test with since
394 $ago = dt_from_string->subtract( days => 9, hours => 23 );
395 my $article_request = $builder->build_object(
397 class => 'Koha::ArticleRequests',
398 value => { borrowernumber => $patron->id, updated_on => $ago },
401 is( $patron->is_active( { days => 9 } ), 0, 'No article requests in 9 days' );
402 is( $patron->is_active( { days => 10 } ), 1, 'Article requests in 10 days' );
403 is( $patron->is_active( { since => $ago } ), 1, 'Article requests since ago' );
404 is( $patron->is_active( { since => $ago->add( days => 1 ) } ), 0, 'No article requests since ago + 1 day' );
405 $article_request->delete;
407 $schema->storage->txn_rollback;
410 subtest 'add_guarantor() tests' => sub {
414 $schema->storage->txn_begin;
416 t::lib::Mocks::mock_preference( 'borrowerRelationship', 'father1|father2' );
418 my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
419 my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
422 { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber }); }
423 'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
424 'Exception is thrown as no relationship passed';
426 is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
429 { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father' }); }
430 'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
431 'Exception is thrown as a wrong relationship was passed';
433 is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
435 $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father1' });
437 my $guarantors = $patron_1->guarantor_relationships;
439 is( $guarantors->count, 1, 'No guarantors added' );
443 open STDERR, '>', '/dev/null';
445 { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father2' }); }
446 'Koha::Exceptions::Patron::Relationship::DuplicateRelationship',
447 'Exception is thrown for duplicated relationship';
451 $schema->storage->txn_rollback;
454 subtest 'relationships_debt() tests' => sub {
458 $schema->storage->txn_begin;
460 t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
462 my $parent_1 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Parent 1" } });
463 my $parent_2 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Parent 2" } });
464 my $child_1 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => " Child 1" } });
465 my $child_2 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => " Child 2" } });
467 $child_1->add_guarantor({ guarantor_id => $parent_1->borrowernumber, relationship => 'parent' });
468 $child_1->add_guarantor({ guarantor_id => $parent_2->borrowernumber, relationship => 'parent' });
469 $child_2->add_guarantor({ guarantor_id => $parent_1->borrowernumber, relationship => 'parent' });
470 $child_2->add_guarantor({ guarantor_id => $parent_2->borrowernumber, relationship => 'parent' });
472 is( $child_1->guarantor_relationships->guarantors->count, 2, 'Child 1 has correct number of guarantors' );
473 is( $child_2->guarantor_relationships->guarantors->count, 2, 'Child 2 has correct number of guarantors' );
474 is( $parent_1->guarantee_relationships->guarantees->count, 2, 'Parent 1 has correct number of guarantees' );
475 is( $parent_2->guarantee_relationships->guarantees->count, 2, 'Parent 2 has correct number of guarantees' );
477 my $patrons = [ $parent_1, $parent_2, $child_1, $child_2 ];
479 # First test: No debt
480 my ($parent1_debt, $parent2_debt, $child1_debt, $child2_debt) = (0,0,0,0);
481 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
483 # Add debt to child_2
485 $child_2->account->add_debit({ type => 'ACCOUNT', amount => $child2_debt, interface => 'commandline' });
486 is( $child_2->account->non_issues_charges, $child2_debt, 'Debt added to Child 2' );
487 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
490 $parent_1->account->add_debit({ type => 'ACCOUNT', amount => $parent1_debt, interface => 'commandline' });
491 is( $parent_1->account->non_issues_charges, $parent1_debt, 'Debt added to Parent 1' );
492 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
495 $parent_2->account->add_debit({ type => 'ACCOUNT', amount => $parent2_debt, interface => 'commandline' });
496 is( $parent_2->account->non_issues_charges, $parent2_debt, 'Parent 2 owes correct amount' );
497 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
500 $child_1->account->add_debit({ type => 'ACCOUNT', amount => $child1_debt, interface => 'commandline' });
501 is( $child_1->account->non_issues_charges, $child1_debt, 'Child 1 owes correct amount' );
502 _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
504 $schema->storage->txn_rollback;
507 sub _test_combinations {
508 my ( $patrons, $parent1_debt, $parent2_debt, $child1_debt, $child2_debt ) = @_;
509 note("Testing with parent 1 debt $parent1_debt | Parent 2 debt $parent2_debt | Child 1 debt $child1_debt | Child 2 debt $child2_debt");
511 # P1 => P1 + C1 + C2 ( - P1 ) ( + P2 )
512 # P2 => P2 + C1 + C2 ( - P2 ) ( + P1 )
513 # C1 => P1 + P2 + C1 + C2 ( - C1 )
514 # C2 => P1 + P2 + C1 + C2 ( - C2 )
516 # 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
517 for my $i ( 0 .. 7 ) {
518 my ( $only_this_guarantor, $include_guarantors, $include_this_patron )
519 = split '', sprintf( "%03b", $i );
520 note("---------------------");
521 for my $patron ( @$patrons ) {
522 if ( $only_this_guarantor
523 && !$patron->guarantee_relationships->count )
526 $patron->relationships_debt(
528 only_this_guarantor => $only_this_guarantor,
529 include_guarantors => $include_guarantors,
530 include_this_patron => $include_this_patron
534 'Koha::Exceptions::BadParameter',
535 'Exception is thrown as patron is not a guarantor';
541 if ( $patron->firstname eq 'Parent 1' ) {
542 $debt += $parent1_debt if ($include_this_patron && $include_guarantors);
543 $debt += $child1_debt + $child2_debt;
544 $debt += $parent2_debt unless ($only_this_guarantor || !$include_guarantors);
546 elsif ( $patron->firstname eq 'Parent 2' ) {
547 $debt += $parent2_debt if ($include_this_patron & $include_guarantors);
548 $debt += $child1_debt + $child2_debt;
549 $debt += $parent1_debt unless ($only_this_guarantor || !$include_guarantors);
551 elsif ( $patron->firstname eq ' Child 1' ) {
552 $debt += $child1_debt if ($include_this_patron);
553 $debt += $child2_debt;
554 $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
557 $debt += $child2_debt if ($include_this_patron);
558 $debt += $child1_debt;
559 $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
563 $patron->relationships_debt(
565 only_this_guarantor => $only_this_guarantor,
566 include_guarantors => $include_guarantors,
567 include_this_patron => $include_this_patron
572 . " debt of " . sprintf('%02d',$debt) . " calculated correctly for ( only_this_guarantor: $only_this_guarantor, include_guarantors: $include_guarantors, include_this_patron: $include_this_patron)"
579 subtest 'add_enrolment_fee_if_needed() tests' => sub {
583 subtest 'category has enrolment fee' => sub {
586 $schema->storage->txn_begin;
588 my $category = $builder->build_object(
590 class => 'Koha::Patron::Categories',
597 my $patron = $builder->build_object(
599 class => 'Koha::Patrons',
601 categorycode => $category->categorycode
606 my $enrollment_fee = $patron->add_enrolment_fee_if_needed();
607 is( $enrollment_fee * 1, 20, 'Enrolment fee amount is correct' );
608 my $account = $patron->account;
609 is( $patron->account->balance * 1, 20, 'Patron charged the enrolment fee' );
610 # second enrolment fee, new
611 $enrollment_fee = $patron->add_enrolment_fee_if_needed(0);
612 # third enrolment fee, renewal
613 $enrollment_fee = $patron->add_enrolment_fee_if_needed(1);
614 is( $patron->account->balance * 1, 60, 'Patron charged the enrolment fees' );
616 my @debits = $account->outstanding_debits->as_list;
617 is( scalar @debits, 3, '3 enrolment fees' );
618 is( $debits[0]->debit_type_code, 'ACCOUNT', 'Account type set correctly' );
619 is( $debits[1]->debit_type_code, 'ACCOUNT', 'Account type set correctly' );
620 is( $debits[2]->debit_type_code, 'ACCOUNT_RENEW', 'Account type set correctly' );
622 $schema->storage->txn_rollback;
625 subtest 'no enrolment fee' => sub {
629 $schema->storage->txn_begin;
631 my $category = $builder->build_object(
633 class => 'Koha::Patron::Categories',
640 my $patron = $builder->build_object(
642 class => 'Koha::Patrons',
644 categorycode => $category->categorycode
649 my $enrollment_fee = $patron->add_enrolment_fee_if_needed();
650 is( $enrollment_fee * 1, 0, 'No enrolment fee' );
651 my $account = $patron->account;
652 is( $patron->account->balance, 0, 'Patron not charged anything' );
654 my @debits = $account->outstanding_debits->as_list;
655 is( scalar @debits, 0, 'no debits' );
657 $schema->storage->txn_rollback;
661 subtest 'messaging_preferences() tests' => sub {
664 $schema->storage->txn_begin;
666 my $mtt = $builder->build_object({
667 class => 'Koha::Patron::MessagePreference::Transport::Types'
669 my $attribute = $builder->build_object({
670 class => 'Koha::Patron::MessagePreference::Attributes'
672 my $branchcode = $builder->build({
673 source => 'Branch' })->{branchcode};
674 my $letter = $builder->build_object({
675 class => 'Koha::Notice::Templates',
679 message_transport_type => $mtt->message_transport_type
683 Koha::Patron::MessagePreference::Transport->new({
684 message_attribute_id => $attribute->message_attribute_id,
685 message_transport_type => $mtt->message_transport_type,
687 letter_module => $letter->module,
688 letter_code => $letter->code,
691 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
693 my $preference = Koha::Patron::MessagePreference->new({
694 borrowernumber => $patron->borrowernumber,
695 message_attribute_id => $attribute->message_attribute_id,
697 days_in_advance => undef,
700 my $messaging_preferences = $patron->messaging_preferences();
701 is($messaging_preferences->count, 1, 'Found one preference');
703 my $messaging_preference = $messaging_preferences->next;
704 is($messaging_preference->borrowernumber, $patron->borrowernumber);
705 is($messaging_preference->message_attribute_id, $attribute->message_attribute_id);
706 is($messaging_preference->wants_digest, 0);
707 is($messaging_preference->days_in_advance, undef);
709 $schema->storage->txn_rollback;
712 subtest 'to_api() tests' => sub {
716 $schema->storage->txn_begin;
718 my $patron_class = Test::MockModule->new('Koha::Patron');
721 sub { return 'algo' }
724 my $patron = $builder->build_object(
726 class => 'Koha::Patrons',
733 my $restricted = $patron->to_api->{restricted};
734 ok( defined $restricted, 'restricted is defined' );
735 ok( !$restricted, 'debarred is undef, restricted evaluates to false' );
737 $patron->debarred( dt_from_string->add( days => 1 ) )->store->discard_changes;
738 $restricted = $patron->to_api->{restricted};
739 ok( defined $restricted, 'restricted is defined' );
740 ok( $restricted, 'debarred is defined, restricted evaluates to true' );
742 my $patron_json = $patron->to_api({ embed => { algo => {} } });
743 ok( exists $patron_json->{algo} );
744 is( $patron_json->{algo}, 'algo' );
746 $schema->storage->txn_rollback;
749 subtest 'login_attempts tests' => sub {
752 $schema->storage->txn_begin;
754 my $patron = $builder->build_object(
756 class => 'Koha::Patrons',
759 my $patron_info = $patron->unblessed;
761 delete $patron_info->{login_attempts};
762 my $new_patron = Koha::Patron->new($patron_info)->store;
763 is( $new_patron->discard_changes->login_attempts, 0, "login_attempts defaults to 0 as expected");
765 $schema->storage->txn_rollback;
768 subtest 'is_superlibrarian() tests' => sub {
772 $schema->storage->txn_begin;
774 my $patron = $builder->build_object(
776 class => 'Koha::Patrons',
784 is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
786 $patron->flags(1)->store->discard_changes;
787 is( $patron->is_superlibrarian, 1, 'Patron is a superlibrarian and the method returns the correct value' );
789 $patron->flags(0)->store->discard_changes;
790 is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
792 $schema->storage->txn_rollback;
795 subtest 'extended_attributes' => sub {
799 my $schema = Koha::Database->new->schema;
800 $schema->storage->txn_begin;
802 Koha::Patron::Attribute::Types->search->delete;
804 my $patron_1 = $builder->build_object({class=> 'Koha::Patrons'});
805 my $patron_2 = $builder->build_object({class=> 'Koha::Patrons'});
807 t::lib::Mocks::mock_userenv({ patron => $patron_1 });
809 my $attribute_type1 = Koha::Patron::Attribute::Type->new(
812 description => 'my description1',
816 my $attribute_type2 = Koha::Patron::Attribute::Type->new(
819 description => 'my description2',
821 staff_searchable => 1
825 my $new_library = $builder->build( { source => 'Branch' } );
826 my $attribute_type_limited = Koha::Patron::Attribute::Type->new(
827 { code => 'my code3', description => 'my description3' } )->store;
828 $attribute_type_limited->library_limits( [ $new_library->{branchcode} ] );
830 my $attributes_for_1 = [
832 attribute => 'my attribute1',
833 code => $attribute_type1->code(),
836 attribute => 'my attribute2',
837 code => $attribute_type2->code(),
840 attribute => 'my attribute limited',
841 code => $attribute_type_limited->code(),
845 my $attributes_for_2 = [
847 attribute => 'my attribute12',
848 code => $attribute_type1->code(),
851 attribute => 'my attribute limited 2',
852 code => $attribute_type_limited->code(),
856 my $extended_attributes = $patron_1->extended_attributes;
857 is( ref($extended_attributes), 'Koha::Patron::Attributes', 'Koha::Patron->extended_attributes must return a Koha::Patron::Attribute set' );
858 is( $extended_attributes->count, 0, 'There should not be attribute yet');
860 $patron_1->extended_attributes->filter_by_branch_limitations->delete;
861 $patron_2->extended_attributes->filter_by_branch_limitations->delete;
862 $patron_1->extended_attributes($attributes_for_1);
863 $patron_2->extended_attributes($attributes_for_2);
865 my $extended_attributes_for_1 = $patron_1->extended_attributes;
866 is( $extended_attributes_for_1->count, 3, 'There should be 3 attributes now for patron 1');
868 my $extended_attributes_for_2 = $patron_2->extended_attributes;
869 is( $extended_attributes_for_2->count, 2, 'There should be 2 attributes now for patron 2');
871 my $attribute_12 = $extended_attributes_for_2->search({ code => $attribute_type1->code })->next;
872 is( $attribute_12->attribute, 'my attribute12', 'search by code should return the correct attribute' );
874 $attribute_12 = $patron_2->get_extended_attribute( $attribute_type1->code );
875 is( $attribute_12->attribute, 'my attribute12', 'Koha::Patron->get_extended_attribute should return the correct attribute value' );
877 my $expected_attributes_for_2 = [
879 code => $attribute_type1->code(),
880 attribute => 'my attribute12',
883 code => $attribute_type_limited->code(),
884 attribute => 'my attribute limited 2',
887 # Sorting them by code
888 $expected_attributes_for_2 = [ sort { $a->{code} cmp $b->{code} } @$expected_attributes_for_2 ];
889 my @extended_attributes_for_2 = $extended_attributes_for_2->as_list;
894 code => $extended_attributes_for_2[0]->code,
895 attribute => $extended_attributes_for_2[0]->attribute
898 code => $extended_attributes_for_2[1]->code,
899 attribute => $extended_attributes_for_2[1]->attribute
902 $expected_attributes_for_2
905 # TODO - What about multiple? POD explains the problem
906 my $non_existent = $patron_2->get_extended_attribute( 'not_exist' );
907 is( $non_existent, undef, 'Koha::Patron->get_extended_attribute must return undef if the attribute does not exist' );
909 # Test branch limitations
910 t::lib::Mocks::mock_userenv({ patron => $patron_2 });
912 $extended_attributes_for_1 = $patron_1->extended_attributes;
913 is( $extended_attributes_for_1->count, 3, 'There should be 2 attributes for patron 1, the limited one should be returned');
916 $extended_attributes_for_1 = $patron_1->extended_attributes->filter_by_branch_limitations;
917 is( $extended_attributes_for_1->count, 2, 'There should be 2 attributes for patron 1, the limited one should be returned');
920 my $limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
921 is( $limited_value->attribute, 'my attribute limited', );
923 ## Do we need a filtered?
924 #$limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
925 #is( $limited_value, undef, );
927 $schema->storage->txn_rollback;
929 subtest 'non-repeatable attributes tests' => sub {
933 $schema->storage->txn_begin;
934 Koha::Patron::Attribute::Types->search->delete;
936 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
937 my $attribute_type = $builder->build_object(
939 class => 'Koha::Patron::Attribute::Types',
940 value => { repeatable => 0 }
944 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
948 $patron->extended_attributes(
950 { code => $attribute_type->code, attribute => 'a' },
951 { code => $attribute_type->code, attribute => 'b' }
955 'Koha::Exceptions::Patron::Attribute::NonRepeatable',
956 'Exception thrown on non-repeatable attribute';
958 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
960 $schema->storage->txn_rollback;
964 subtest 'unique attributes tests' => sub {
968 $schema->storage->txn_begin;
969 Koha::Patron::Attribute::Types->search->delete;
971 my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
972 my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
974 my $attribute_type_1 = $builder->build_object(
976 class => 'Koha::Patron::Attribute::Types',
977 value => { unique_id => 1 }
981 my $attribute_type_2 = $builder->build_object(
983 class => 'Koha::Patron::Attribute::Types',
984 value => { unique_id => 0 }
988 is( $patron_1->extended_attributes->count, 0, 'patron_1 has no extended attributes' );
989 is( $patron_2->extended_attributes->count, 0, 'patron_2 has no extended attributes' );
991 $patron_1->extended_attributes(
993 { code => $attribute_type_1->code, attribute => 'a' },
994 { code => $attribute_type_2->code, attribute => 'a' }
1000 $patron_2->extended_attributes(
1002 { code => $attribute_type_1->code, attribute => 'a' },
1003 { code => $attribute_type_2->code, attribute => 'a' }
1007 'Koha::Exceptions::Patron::Attribute::UniqueIDConstraint',
1008 'Exception thrown on unique attribute';
1010 is( $patron_1->extended_attributes->count, 2, 'Extended attributes stored' );
1011 is( $patron_2->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
1013 $schema->storage->txn_rollback;
1017 subtest 'invalid type attributes tests' => sub {
1021 $schema->storage->txn_begin;
1022 Koha::Patron::Attribute::Types->search->delete;
1024 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1026 my $attribute_type_1 = $builder->build_object(
1028 class => 'Koha::Patron::Attribute::Types',
1029 value => { repeatable => 0 }
1033 my $attribute_type_2 = $builder->build_object(
1035 class => 'Koha::Patron::Attribute::Types'
1039 my $type_2 = $attribute_type_2->code;
1040 $attribute_type_2->delete;
1042 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
1046 $patron->extended_attributes(
1048 { code => $attribute_type_1->code, attribute => 'a' },
1049 { code => $attribute_type_2->code, attribute => 'b' }
1053 'Koha::Exceptions::Patron::Attribute::InvalidType',
1054 'Exception thrown on invalid attribute type';
1056 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
1058 $schema->storage->txn_rollback;
1062 subtest 'globally mandatory attributes tests' => sub {
1066 $schema->storage->txn_begin;
1067 Koha::Patron::Attribute::Types->search->delete;
1069 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1071 my $attribute_type_1 = $builder->build_object(
1073 class => 'Koha::Patron::Attribute::Types',
1074 value => { mandatory => 1, class => 'a', category_code => undef }
1078 my $attribute_type_2 = $builder->build_object(
1080 class => 'Koha::Patron::Attribute::Types',
1081 value => { mandatory => 0, class => 'a', category_code => undef }
1085 is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
1089 $patron->extended_attributes(
1091 { code => $attribute_type_2->code, attribute => 'b' }
1095 'Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute',
1096 'Exception thrown on missing mandatory attribute type';
1098 is( $@->type, $attribute_type_1->code, 'Exception parameters are correct' );
1100 is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
1102 $patron->extended_attributes(
1104 { code => $attribute_type_1->code, attribute => 'b' }
1108 is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
1110 $schema->storage->txn_rollback;
1114 subtest 'limited category mandatory attributes tests' => sub {
1118 $schema->storage->txn_begin;
1119 Koha::Patron::Attribute::Types->search->delete;
1121 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1123 my $attribute_type_1 = $builder->build_object(
1125 class => 'Koha::Patron::Attribute::Types',
1126 value => { mandatory => 1, class => 'a', category_code => $patron->categorycode }
1130 $patron->extended_attributes(
1132 { code => $attribute_type_1->code, attribute => 'a' }
1136 is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
1138 $patron = $builder->build_object({ class => 'Koha::Patrons' });
1139 # new patron, new category - they shouldn't be required to have any attributes
1142 ok( $patron->extended_attributes([]), "We can set no attributes, mandatory attribute for other category not required");
1151 subtest 'can_log_into() tests' => sub {
1155 $schema->storage->txn_begin;
1157 my $patron = $builder->build_object(
1159 class => 'Koha::Patrons',
1165 my $library = $builder->build_object({ class => 'Koha::Libraries' });
1167 t::lib::Mocks::mock_preference('IndependentBranches', 1);
1169 ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
1170 ok( !$patron->can_log_into( $library ), 'Patron cannot log into different library, IndependentBranches on' );
1172 # make it a superlibrarian
1173 $patron->set({ flags => 1 })->store->discard_changes;
1174 ok( $patron->can_log_into( $library ), 'Superlibrarian can log into different library, IndependentBranches on' );
1176 t::lib::Mocks::mock_preference('IndependentBranches', 0);
1178 # No special permissions
1179 $patron->set({ flags => undef })->store->discard_changes;
1180 ok( $patron->can_log_into( $patron->library ), 'Patron can log into its own library' );
1181 ok( $patron->can_log_into( $library ), 'Patron can log into any library' );
1183 $schema->storage->txn_rollback;
1186 subtest 'can_request_article() tests' => sub {
1190 $schema->storage->txn_begin;
1192 t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
1194 my $item = $builder->build_sample_item;
1196 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1197 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1198 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1200 t::lib::Mocks::mock_userenv( { branchcode => $library_2->id } );
1202 Koha::CirculationRules->set_rule(
1204 categorycode => undef,
1205 branchcode => $library_1->id,
1206 rule_name => 'open_article_requests_limit',
1211 $builder->build_object(
1213 class => 'Koha::ArticleRequests',
1214 value => { status => 'REQUESTED', borrowernumber => $patron->id }
1217 $builder->build_object(
1219 class => 'Koha::ArticleRequests',
1220 value => { status => 'PENDING', borrowernumber => $patron->id }
1223 $builder->build_object(
1225 class => 'Koha::ArticleRequests',
1226 value => { status => 'PROCESSING', borrowernumber => $patron->id }
1229 $builder->build_object(
1231 class => 'Koha::ArticleRequests',
1232 value => { status => 'CANCELED', borrowernumber => $patron->id }
1237 $patron->can_request_article( $library_1->id ),
1238 '3 current requests, 4 is the limit: allowed'
1241 # Completed request, same day
1242 my $completed = $builder->build_object(
1244 class => 'Koha::ArticleRequests',
1246 status => 'COMPLETED',
1247 borrowernumber => $patron->id
1252 ok( !$patron->can_request_article( $library_1->id ),
1253 '3 current requests and a completed one the same day: denied' );
1255 $completed->updated_on(
1256 dt_from_string->add( days => -1 )->set(
1263 ok( $patron->can_request_article( $library_1->id ),
1264 '3 current requests and a completed one the day before: allowed' );
1266 Koha::CirculationRules->set_rule(
1268 categorycode => undef,
1269 branchcode => $library_2->id,
1270 rule_name => 'open_article_requests_limit',
1275 ok( !$patron->can_request_article,
1276 'Not passing the library_id param makes it fallback to userenv: denied'
1279 $schema->storage->txn_rollback;
1282 subtest 'article_requests() tests' => sub {
1286 $schema->storage->txn_begin;
1288 my $library = $builder->build_object({ class => 'Koha::Libraries' });
1289 t::lib::Mocks::mock_userenv( { branchcode => $library->id } );
1291 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1293 my $article_requests = $patron->article_requests;
1294 is( ref($article_requests), 'Koha::ArticleRequests',
1295 'In scalar context, type is correct' );
1296 is( $article_requests->count, 0, 'No article requests' );
1298 foreach my $i ( 0 .. 3 ) {
1300 my $item = $builder->build_sample_item;
1302 Koha::ArticleRequest->new(
1304 borrowernumber => $patron->id,
1305 biblionumber => $item->biblionumber,
1306 itemnumber => $item->id,
1312 $article_requests = $patron->article_requests;
1313 is( $article_requests->count, 4, '4 article requests' );
1315 $schema->storage->txn_rollback;
1319 subtest 'can_patron_change_staff_only_lists() tests' => sub {
1323 $schema->storage->txn_begin;
1325 # make a user with no special permissions
1326 my $patron = $builder->build_object(
1328 class => 'Koha::Patrons',
1334 is( $patron->can_patron_change_staff_only_lists(), 0, 'Patron without permissions cannot change staff only lists');
1336 # make it a 'Catalogue' permission
1337 $patron->set({ flags => 4 })->store->discard_changes;
1338 is( $patron->can_patron_change_staff_only_lists(), 1, 'Catalogue patron can change staff only lists');
1341 # make it a superlibrarian
1342 $patron->set({ flags => 1 })->store->discard_changes;
1343 is( $patron->can_patron_change_staff_only_lists(), 1, 'Superlibrarian patron can change staff only lists');
1345 $schema->storage->txn_rollback;
1348 subtest 'can_patron_change_permitted_staff_lists() tests' => sub {
1352 $schema->storage->txn_begin;
1354 # make a user with no special permissions
1355 my $patron = $builder->build_object(
1357 class => 'Koha::Patrons',
1363 is( $patron->can_patron_change_permitted_staff_lists(), 0, 'Patron without permissions cannot change permitted staff lists');
1365 # make it a 'Catalogue' permission
1366 $patron->set({ flags => 4 })->store->discard_changes;
1367 is( $patron->can_patron_change_permitted_staff_lists(), 0, 'Catalogue patron cannot change permitted staff lists');
1369 # make it a 'Catalogue' permission and 'edit_public_list_contents' sub-permission
1370 $patron->set({ flags => 4 })->store->discard_changes;
1373 source => 'UserPermission',
1375 borrowernumber => $patron->borrowernumber,
1376 module_bit => 20, # lists
1377 code => 'edit_public_list_contents',
1381 is( $patron->can_patron_change_permitted_staff_lists(), 1, 'Catalogue and "edit_public_list_contents" patron can change permitted staff lists');
1383 # make it a superlibrarian
1384 $patron->set({ flags => 1 })->store->discard_changes;
1385 is( $patron->can_patron_change_permitted_staff_lists(), 1, 'Superlibrarian patron can change permitted staff lists');
1387 $schema->storage->txn_rollback;
1390 subtest 'password expiration tests' => sub {
1394 $schema->storage->txn_begin;
1395 my $date = dt_from_string();
1396 my $category = $builder->build_object({ class => 'Koha::Patron::Categories', value => {
1397 password_expiry_days => 10,
1398 require_strong_password => 0,
1401 my $patron = $builder->build_object({ class=> 'Koha::Patrons', value => {
1402 categorycode => $category->categorycode,
1407 $patron->delete()->store()->discard_changes(); # Make sure we are storing a 'new' patron
1409 is( $patron->password_expiration_date(), $date->add( days => 10 )->ymd() , "Password expiration date set correctly on patron creation");
1411 $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1412 categorycode => $category->categorycode,
1416 $patron->delete()->store()->discard_changes();
1418 is( $patron->password_expiration_date(), undef, "Password expiration date is not set if patron does not have a password");
1420 $category->password_expiry_days(undef)->store();
1421 $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1422 categorycode => $category->categorycode
1425 $patron->delete()->store()->discard_changes();
1426 is( $patron->password_expiration_date(), undef, "Password expiration date is not set if category does not have expiry days set");
1428 $schema->storage->txn_rollback;
1430 subtest 'password_expired' => sub {
1434 $schema->storage->txn_begin;
1435 my $date = dt_from_string();
1436 $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1437 password_expiration_date => undef
1440 is( $patron->password_expired, 0, "Patron with no password expiration date, password not expired");
1441 $patron->password_expiration_date( $date )->store;
1442 $patron->discard_changes();
1443 is( $patron->password_expired, 1, "Patron with password expiration date of today, password expired");
1444 $date->subtract( days => 1 );
1445 $patron->password_expiration_date( $date )->store;
1446 $patron->discard_changes();
1447 is( $patron->password_expired, 1, "Patron with password expiration date in past, password expired");
1449 $schema->storage->txn_rollback;
1452 subtest 'set_password' => sub {
1456 $schema->storage->txn_begin;
1458 my $date = dt_from_string();
1459 my $category = $builder->build_object({ class => 'Koha::Patron::Categories', value => {
1460 password_expiry_days => 10
1463 my $patron = $builder->build_object({ class => 'Koha::Patrons', value => {
1464 categorycode => $category->categorycode,
1465 password_expiration_date => $date->subtract( days => 1 )
1468 is( $patron->password_expired, 1, "Patron password is expired");
1470 $date = dt_from_string();
1471 $patron->set_password({ password => "kitten", skip_validation => 1 })->discard_changes();
1472 is( $patron->password_expired, 0, "Patron password no longer expired when new password set");
1473 is( $patron->password_expiration_date(), $date->add( days => 10 )->ymd(), "Password expiration date set correctly on patron creation");
1476 $category->password_expiry_days( undef )->store();
1477 $patron->set_password({ password => "puppies", skip_validation => 1 })->discard_changes();
1478 is( $patron->password_expiration_date(), undef, "Password expiration date is unset if category does not have expiry days");
1480 $schema->storage->txn_rollback;
1485 subtest 'safe_to_delete() tests' => sub {
1489 $schema->storage->txn_begin;
1491 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1493 ## Make it the anonymous
1494 t::lib::Mocks::mock_preference( 'AnonymousPatron', $patron->id );
1496 ok( !$patron->safe_to_delete, 'Cannot delete, it is the anonymous patron' );
1497 my $message = $patron->safe_to_delete->messages->[0];
1498 is( $message->type, 'error', 'Type is error' );
1499 is( $message->message, 'is_anonymous_patron', 'Cannot delete, it is the anonymous patron' );
1501 t::lib::Mocks::mock_preference( 'AnonymousPatron', 0 );
1503 ## Make it have a checkout
1504 my $checkout = $builder->build_object(
1506 class => 'Koha::Checkouts',
1507 value => { borrowernumber => $patron->id }
1511 ok( !$patron->safe_to_delete, 'Cannot delete, has checkouts' );
1512 $message = $patron->safe_to_delete->messages->[0];
1513 is( $message->type, 'error', 'Type is error' );
1514 is( $message->message, 'has_checkouts', 'Cannot delete, has checkouts' );
1518 ## Make it have a guarantee
1519 t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
1520 $builder->build_object({ class => 'Koha::Patrons' })
1521 ->add_guarantor({ guarantor_id => $patron->id, relationship => 'parent' });
1523 ok( !$patron->safe_to_delete, 'Cannot delete, has guarantees' );
1524 $message = $patron->safe_to_delete->messages->[0];
1525 is( $message->type, 'error', 'Type is error' );
1526 is( $message->message, 'has_guarantees', 'Cannot delete, has guarantees' );
1529 $patron->guarantee_relationships->delete;
1531 ## Make it have debt
1532 my $debit = $patron->account->add_debit({ amount => 10, interface => 'intranet', type => 'MANUAL' });
1534 ok( !$patron->safe_to_delete, 'Cannot delete, has debt' );
1535 $message = $patron->safe_to_delete->messages->[0];
1536 is( $message->type, 'error', 'Type is error' );
1537 is( $message->message, 'has_debt', 'Cannot delete, has debt' );
1539 my $manager = $builder->build_object( { class => 'Koha::Patrons' } );
1540 t::lib::Mocks::mock_userenv( { borrowernumber => $manager->id } );
1541 $patron->account->pay({ amount => 10, debits => [ $debit ] });
1544 ok( $patron->safe_to_delete, 'Can delete, all conditions met' );
1545 my $messages = $patron->safe_to_delete->messages;
1546 is_deeply( $messages, [], 'Patron can be deleted, no messages' );
1548 $schema->storage->txn_rollback;
1551 subtest 'article_request_fee() tests' => sub {
1555 $schema->storage->txn_begin;
1557 # Cleanup, to avoid interference
1558 Koha::CirculationRules->search( { rule_name => 'article_request_fee' } )->delete;
1560 t::lib::Mocks::mock_preference( 'ArticleRequests', 1 );
1562 my $item = $builder->build_sample_item;
1564 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1565 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1566 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1568 # Rule that should never be picked, because the patron's category is always picked
1569 Koha::CirculationRules->set_rule(
1570 { categorycode => undef,
1571 branchcode => undef,
1572 rule_name => 'article_request_fee',
1577 is( $patron->article_request_fee( { library_id => $library_2->id } ), 1, 'library_id used correctly' );
1579 Koha::CirculationRules->set_rule(
1580 { categorycode => $patron->categorycode,
1581 branchcode => undef,
1582 rule_name => 'article_request_fee',
1587 Koha::CirculationRules->set_rule(
1588 { categorycode => $patron->categorycode,
1589 branchcode => $library_1->id,
1590 rule_name => 'article_request_fee',
1595 is( $patron->article_request_fee( { library_id => $library_2->id } ), 2, 'library_id used correctly' );
1597 t::lib::Mocks::mock_userenv( { branchcode => $library_1->id } );
1599 is( $patron->article_request_fee(), 3, 'env used correctly' );
1601 $schema->storage->txn_rollback;
1604 subtest 'add_article_request_fee_if_needed() tests' => sub {
1608 $schema->storage->txn_begin;
1612 my $patron_mock = Test::MockModule->new('Koha::Patron');
1613 $patron_mock->mock( 'article_request_fee', sub { return $amount; } );
1615 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1617 is( $patron->article_request_fee, $amount, 'article_request_fee mocked' );
1619 my $library_1 = $builder->build_object( { class => 'Koha::Libraries' } );
1620 my $library_2 = $builder->build_object( { class => 'Koha::Libraries' } );
1621 my $staff = $builder->build_object( { class => 'Koha::Patrons' } );
1622 my $item = $builder->build_sample_item;
1624 t::lib::Mocks::mock_userenv(
1625 { branchcode => $library_1->id, patron => $staff } );
1627 my $debit = $patron->add_article_request_fee_if_needed();
1628 is( $debit, undef, 'No fee, no debit line' );
1633 $debit = $patron->add_article_request_fee_if_needed({ item_id => $item->id });
1634 is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1635 is( $debit->amount, $amount,
1636 'amount set to $patron->article_request_fee value' );
1637 is( $debit->manager_id, $staff->id,
1638 'manager_id set to userenv session user' );
1639 is( $debit->branchcode, $library_1->id,
1640 'branchcode set to userenv session library' );
1641 is( $debit->debit_type_code, 'ARTICLE_REQUEST',
1642 'debit_type_code set correctly' );
1643 is( $debit->itemnumber, $item->id,
1644 'itemnumber set correctly' );
1648 $debit = $patron->add_article_request_fee_if_needed({ library_id => $library_2->id });
1649 is( ref($debit), 'Koha::Account::Line', 'Debit object type correct' );
1650 is( $debit->amount, $amount,
1651 'amount set to $patron->article_request_fee value' );
1652 is( $debit->branchcode, $library_2->id,
1653 'branchcode set to userenv session library' );
1654 is( $debit->itemnumber, undef,
1655 'itemnumber set correctly to undef' );
1657 $schema->storage->txn_rollback;
1660 subtest 'messages' => sub {
1663 $schema->storage->txn_begin;
1665 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1666 my $messages = $patron->messages;
1667 is( $messages->count, 0, "No message yet" );
1668 my $message_1 = $builder->build_object(
1670 class => 'Koha::Patron::Messages',
1671 value => { borrowernumber => $patron->borrowernumber }
1674 my $message_2 = $builder->build_object(
1676 class => 'Koha::Patron::Messages',
1677 value => { borrowernumber => $patron->borrowernumber }
1681 $messages = $patron->messages;
1682 is( $messages->count, 2, "There are two messages for this patron" );
1683 is( $messages->next->message, $message_1->message );
1684 is( $messages->next->message, $message_2->message );
1685 $schema->storage->txn_rollback;
1688 subtest 'recalls() tests' => sub {
1692 $schema->storage->txn_begin;
1694 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1695 my $biblio1 = $builder->build_object({ class => 'Koha::Biblios' });
1696 my $item1 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio1->biblionumber } });
1697 my $biblio2 = $builder->build_object({ class => 'Koha::Biblios' });
1698 my $item2 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio2->biblionumber } });
1701 { biblio_id => $biblio1->biblionumber,
1702 patron_id => $patron->borrowernumber,
1703 item_id => $item1->itemnumber,
1704 pickup_library_id => $patron->branchcode,
1705 created_date => \'NOW()',
1710 { biblio_id => $biblio2->biblionumber,
1711 patron_id => $patron->borrowernumber,
1712 item_id => $item2->itemnumber,
1713 pickup_library_id => $patron->branchcode,
1714 created_date => \'NOW()',
1719 { biblio_id => $biblio1->biblionumber,
1720 patron_id => $patron->borrowernumber,
1722 pickup_library_id => $patron->branchcode,
1723 created_date => \'NOW()',
1727 my $recall = Koha::Recall->new(
1728 { biblio_id => $biblio1->biblionumber,
1729 patron_id => $patron->borrowernumber,
1731 pickup_library_id => $patron->branchcode,
1732 created_date => \'NOW()',
1736 $recall->set_cancelled;
1738 is( $patron->recalls->count, 4, "Correctly gets this patron's recalls" );
1739 is( $patron->recalls->filter_by_current->count, 3, "Correctly gets this patron's active recalls" );
1740 is( $patron->recalls->filter_by_current->search( { biblio_id => $biblio1->biblionumber } )->count, 2, "Correctly gets this patron's active recalls on a specific biblio" );
1742 $schema->storage->txn_rollback;
1745 subtest 'encode_secret and decoded_secret' => sub {
1747 $schema->storage->txn_begin;
1749 t::lib::Mocks::mock_config('encryption_key', 't0P_secret');
1751 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1752 is( $patron->decoded_secret, undef, 'TestBuilder does not initialize it' );
1753 $patron->secret(q{});
1754 is( $patron->decoded_secret, q{}, 'Empty string case' );
1756 $patron->encode_secret('encrypt_me'); # Note: lazy testing; should be base32 string normally.
1757 is( length($patron->secret) > 0, 1, 'Secret length' );
1758 isnt( $patron->secret, 'encrypt_me', 'Encrypted column' );
1759 is( $patron->decoded_secret, 'encrypt_me', 'Decrypted column' );
1761 $schema->storage->txn_rollback;
1764 subtest 'notify_library_of_registration()' => sub {
1768 $schema->storage->txn_begin;
1769 my $dbh = C4::Context->dbh;
1771 my $library = $builder->build_object(
1773 class => 'Koha::Libraries',
1775 branchemail => 'from@mybranch.com',
1776 branchreplyto => 'to@mybranch.com'
1780 my $patron = $builder->build_object(
1782 class => 'Koha::Patrons',
1784 branchcode => $library->branchcode
1789 t::lib::Mocks::mock_preference( 'KohaAdminEmailAddress', 'root@localhost' );
1790 t::lib::Mocks::mock_preference( 'EmailAddressForPatronRegistrations', 'library@localhost' );
1792 # Test when EmailPatronRegistrations equals BranchEmailAddress
1793 t::lib::Mocks::mock_preference( 'EmailPatronRegistrations', 'BranchEmailAddress' );
1794 is( $patron->notify_library_of_registration(C4::Context->preference('EmailPatronRegistrations')), 1, 'OPAC_REG email is queued if EmailPatronRegistration syspref equals BranchEmailAddress');
1795 my $sth = $dbh->prepare("SELECT to_address FROM message_queue where borrowernumber = ?");
1796 $sth->execute( $patron->borrowernumber );
1797 my $to_address = $sth->fetchrow_array;
1798 is( $to_address, 'to@mybranch.com', 'OPAC_REG email queued to go to branchreplyto address when EmailPatronRegistration equals BranchEmailAddress' );
1799 $dbh->do(q|DELETE FROM message_queue|);
1801 # Test when EmailPatronRegistrations equals EmailAddressForPatronRegistrations
1802 t::lib::Mocks::mock_preference( 'EmailPatronRegistrations', 'EmailAddressForPatronRegistrations' );
1803 is( $patron->notify_library_of_registration(C4::Context->preference('EmailPatronRegistrations')), 1, 'OPAC_REG email is queued if EmailPatronRegistration syspref equals EmailAddressForPatronRegistrations');
1804 $sth->execute( $patron->borrowernumber );
1805 $to_address = $sth->fetchrow_array;
1806 is( $to_address, 'library@localhost', 'OPAC_REG email queued to go to EmailAddressForPatronRegistrations syspref when EmailPatronRegistration equals EmailAddressForPatronRegistrations' );
1807 $dbh->do(q|DELETE FROM message_queue|);
1809 # Test when EmailPatronRegistrations equals KohaAdminEmailAddress
1810 t::lib::Mocks::mock_preference( 'EmailPatronRegistrations', 'KohaAdminEmailAddress' );
1811 t::lib::Mocks::mock_preference( 'ReplyToDefault', 'root@localhost' ); # FIXME Remove localhost
1812 is( $patron->notify_library_of_registration(C4::Context->preference('EmailPatronRegistrations')), 1, 'OPAC_REG email is queued if EmailPatronRegistration syspref equals KohaAdminEmailAddress');
1813 $sth->execute( $patron->borrowernumber );
1814 $to_address = $sth->fetchrow_array;
1815 is( $to_address, 'root@localhost', 'OPAC_REG email queued to go to KohaAdminEmailAddress syspref when EmailPatronRegistration equals KohaAdminEmailAddress' );
1816 $dbh->do(q|DELETE FROM message_queue|);
1818 $schema->storage->txn_rollback;
1821 subtest 'notice_email_address' => sub {
1823 $schema->storage->txn_begin;
1825 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1827 t::lib::Mocks::mock_preference( 'EmailFieldPrecedence', 'email|emailpro' );
1828 t::lib::Mocks::mock_preference( 'EmailFieldPrimary', 'OFF' );
1829 is ($patron->notice_email_address, $patron->email, "Koha::Patron->notice_email_address returns correct value when EmailFieldPrimary is off");
1831 t::lib::Mocks::mock_preference( 'EmailFieldPrimary', 'emailpro' );
1832 is ($patron->notice_email_address, $patron->emailpro, "Koha::Patron->notice_email_address returns correct value when EmailFieldPrimary is emailpro");
1835 $schema->storage->txn_rollback;
1838 subtest 'first_valid_email_address' => sub {
1840 $schema->storage->txn_begin;
1842 my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { emailpro => ''}});
1844 t::lib::Mocks::mock_preference( 'EmailFieldPrecedence', 'emailpro|email' );
1845 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");
1848 $schema->storage->txn_rollback;
1851 subtest 'get_savings tests' => sub {
1855 $schema->storage->txn_begin;
1857 my $library = $builder->build_object({ class => 'Koha::Libraries' });
1858 my $patron = $builder->build_object({ class => 'Koha::Patrons' }, { value => { branchcode => $library->branchcode } });
1860 t::lib::Mocks::mock_userenv({ patron => $patron, branchcode => $library->branchcode });
1862 my $biblio = $builder->build_sample_biblio;
1863 my $item1 = $builder->build_sample_item(
1865 biblionumber => $biblio->biblionumber,
1866 library => $library->branchcode,
1867 replacementprice => rand(20),
1870 my $item2 = $builder->build_sample_item(
1872 biblionumber => $biblio->biblionumber,
1873 library => $library->branchcode,
1874 replacementprice => rand(20),
1878 is( $patron->get_savings, 0, 'No checkouts, no savings' );
1880 # Add an old checkout with deleted itemnumber
1881 $builder->build_object({ class => 'Koha::Old::Checkouts', value => { itemnumber => undef, borrowernumber => $patron->id } });
1883 is( $patron->get_savings, 0, 'No checkouts with itemnumber, no savings' );
1885 AddIssue( $patron, $item1->barcode );
1886 AddIssue( $patron, $item2->barcode );
1888 my $savings = $patron->get_savings;
1889 is( $savings + 0, $item1->replacementprice + $item2->replacementprice, "Savings correctly calculated from current issues" );
1891 AddReturn( $item2->barcode, $item2->homebranch );
1893 $savings = $patron->get_savings;
1894 is( $savings + 0, $item1->replacementprice + $item2->replacementprice, "Savings correctly calculated from current and old issues" );
1896 $schema->storage->txn_rollback;
1899 subtest 'update privacy tests' => sub {
1900 $schema->storage->txn_begin;
1904 my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { privacy => 1 } });
1906 my $old_checkout = $builder->build_object({ class => 'Koha::Old::Checkouts', value => { borrowernumber => $patron->id } });
1908 t::lib::Mocks::mock_preference( 'AnonymousPatron', '0' );
1910 $patron->privacy(2); #set to never
1912 throws_ok{ $patron->store } 'Koha::Exceptions::Patron::FailedAnonymizing', 'We throw an exception when anonymizing fails';
1914 $old_checkout->discard_changes; #refresh from db
1915 $patron->discard_changes;
1917 is( $old_checkout->borrowernumber, $patron->id, "When anonymizing fails, we don't clear the checkouts");
1918 is( $patron->privacy(), 1, "When anonymizing fails, we don't chaneg the privacy");
1920 my $anon_patron = $builder->build_object({ class => 'Koha::Patrons'});
1921 t::lib::Mocks::mock_preference( 'AnonymousPatron', $anon_patron->id );
1923 $patron->privacy(2)->store(); #set to never
1925 $old_checkout->discard_changes; #refresh from db
1926 $patron->discard_changes;
1928 is( $old_checkout->borrowernumber, $anon_patron->id, "Checkout is successfully anonymized");
1929 is( $patron->privacy(), 2, "Patron privacy is successfully updated");
1931 $schema->storage->txn_rollback;
1934 subtest 'alert_subscriptions tests' => sub {
1938 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1940 my $subscription1 = $builder->build_object( { class => 'Koha::Subscriptions' } );
1941 $subscription1->add_subscriber($patron);
1943 my $subscription2 = $builder->build_object( { class => 'Koha::Subscriptions' } );
1944 $subscription2->add_subscriber($patron);
1946 my @subscriptions = $patron->alert_subscriptions->as_list;
1948 is( @subscriptions, 2, "Number of patron's subscribed alerts successfully fetched" );
1949 is( $subscriptions[0]->subscriptionid, $subscription1->subscriptionid, "First subscribed alert is correct" );
1950 is( $subscriptions[1]->subscriptionid, $subscription2->subscriptionid, "Second subscribed alert is correct" );
1952 $patron->discard_changes;
1955 subtest 'update_lastseen tests' => sub {
1958 $schema->storage->txn_begin;
1960 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1961 my $userid = $patron->userid;
1963 $patron->lastseen( undef );
1966 my $cache = Koha::Caches->get_instance();
1967 my $cache_key = "track_login_" . $patron->userid;
1968 $cache->clear_from_cache($cache_key);
1970 t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', '1' );
1971 t::lib::Mocks::mock_preference( 'TrackLastPatronActivityTriggers', 'login,connection,check_in,check_out,renewal' );
1973 is( $patron->lastseen, undef, 'Patron should have not last seen when newly created' );
1975 my $now = dt_from_string();
1976 Time::Fake->offset( $now->epoch );
1978 $patron->update_lastseen( 'login' );
1979 $patron->_result()->discard_changes();
1980 isnt( $patron->lastseen, undef, 'Patron should have last seen set when TrackLastPatronActivity = 1' );
1981 my $last_seen = $patron->lastseen;
1983 Time::Fake->offset( $now->epoch + 5 );
1984 # Test that lastseen isn't updated more than once in a day (regardless of activity passed)
1985 $patron->update_lastseen( 'login' );
1986 $patron->_result()->discard_changes();
1987 is( $patron->lastseen, $last_seen, 'Patron last seen should still be unchanged after a login' );
1988 $patron->update_lastseen( 'connection' );
1989 $patron->_result()->discard_changes();
1990 is( $patron->lastseen, $last_seen, 'Patron last seen should still be unchanged after a SIP/ILSDI connection' );
1991 $patron->update_lastseen( 'check_out' );
1992 $patron->_result()->discard_changes();
1993 is( $patron->lastseen, $last_seen, 'Patron last seen should still be unchanged after a check out' );
1994 $patron->update_lastseen( 'check_in' );
1995 $patron->_result()->discard_changes();
1996 is( $patron->lastseen, $last_seen, 'Patron last seen should still be unchanged after a check in' );
1997 $patron->update_lastseen( 'renewal' );
1998 $patron->_result()->discard_changes();
1999 is( $patron->lastseen, $last_seen, 'Patron last seen should still be unchanged after a renewal' );
2001 # Check that tracking is disabled when the activity isn't listed
2002 t::lib::Mocks::mock_preference( 'TrackLastPatronActivityTriggers', '' );
2003 $cache->clear_from_cache($cache_key); # Rule out the more than once day prevention above
2005 $patron->update_lastseen( 'login' );
2006 $patron->_result()->discard_changes();
2007 is( $patron->lastseen, $last_seen, 'Patron last seen should be unchanged after a login if login is not selected as an option and the cache has been cleared' );
2009 $patron->update_lastseen( 'connection' );
2010 $patron->_result()->discard_changes();
2011 is( $patron->lastseen, $last_seen, 'Patron last seen should be unchanged after a connection if connection is not selected as an option and the cache has been cleared' );
2013 $patron->update_lastseen( 'check_in' );
2014 $patron->_result()->discard_changes();
2015 is( $patron->lastseen, $last_seen, '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' );
2017 $patron->update_lastseen( 'check_out' );
2018 $patron->_result()->discard_changes();
2019 is( $patron->lastseen, $last_seen, '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' );
2021 $patron->update_lastseen( 'renewal' );
2022 $patron->_result()->discard_changes();
2023 is( $patron->lastseen, $last_seen, 'Patron last seen should be unchanged after a renewal if renewal is not selected as an option and the cache has been cleared' );
2025 # Check tracking for each activity
2026 t::lib::Mocks::mock_preference( 'TrackLastPatronActivityTriggers', 'login,connection,check_in,check_out,renewal' );
2028 $cache->clear_from_cache($cache_key);
2029 $patron->update_lastseen( 'login' );
2030 $patron->_result()->discard_changes();
2031 isnt( $patron->lastseen, $last_seen, 'Patron last seen should be changed after a login if we cleared the cache' );
2033 $cache->clear_from_cache($cache_key);
2034 $patron->update_lastseen( 'connection' );
2035 $patron->_result()->discard_changes();
2036 isnt( $patron->lastseen, $last_seen, 'Patron last seen should be changed after a connection if we cleared the cache' );
2038 $cache->clear_from_cache($cache_key);
2039 $patron->update_lastseen( 'check_out' );
2040 $patron->_result()->discard_changes();
2041 isnt( $patron->lastseen, $last_seen, 'Patron last seen should be changed after a check_out if we cleared the cache' );
2043 $cache->clear_from_cache($cache_key);
2044 $patron->update_lastseen( 'check_in' );
2045 $patron->_result()->discard_changes();
2046 isnt( $patron->lastseen, $last_seen, 'Patron last seen should be changed after a check_in if we cleared the cache' );
2048 $cache->clear_from_cache($cache_key);
2049 $patron->update_lastseen( 'renewal' );
2050 $patron->_result()->discard_changes();
2051 isnt( $patron->lastseen, $last_seen, 'Patron last seen should be changed after a renewal if we cleared the cache' );
2053 # Check that the preference takes effect
2054 t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', '0' );
2055 $patron->lastseen( undef )->store;
2056 $cache->clear_from_cache($cache_key);
2057 $patron->update_lastseen( 'login' );
2058 $patron->_result()->discard_changes();
2059 is( $patron->lastseen, undef, 'Patron should still have last seen unchanged when TrackLastPatronActivity = 0' );
2062 $schema->storage->txn_rollback;