3 # Copyright 2018 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 => 8;
27 use Koha::Account::Lines;
28 use Koha::Account::Offsets;
32 use t::lib::TestBuilder;
34 my $schema = Koha::Database->new->schema;
35 my $builder = t::lib::TestBuilder->new;
36 C4::Context->interface('commandline');
38 subtest 'new' => sub {
42 $schema->storage->txn_begin;
44 throws_ok { Koha::Account->new(); } qr/No patron id passed in!/, 'Croaked on bad call to new';
46 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
47 my $account = Koha::Account->new( { patron_id => $patron->borrowernumber } );
48 is( defined $account, 1, "Account is defined" );
50 $schema->storage->txn_rollback;
53 subtest 'outstanding_debits() tests' => sub {
57 $schema->storage->txn_begin;
59 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
60 my $account = $patron->account;
63 push @generated_lines, Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 1, amountoutstanding => 1, interface => 'commandline' })->store;
64 push @generated_lines, Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 2, amountoutstanding => 2, interface => 'commandline' })->store;
65 push @generated_lines, Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 3, amountoutstanding => 3, interface => 'commandline' })->store;
66 push @generated_lines, Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 4, amountoutstanding => 4, interface => 'commandline' })->store;
68 my $lines = $account->outstanding_debits();
69 my @lines_arr = $account->outstanding_debits();
71 is( ref($lines), 'Koha::Account::Lines', 'Called in scalar context, outstanding_debits returns a Koha::Account::Lines object' );
72 is( $lines->total_outstanding, 10, 'Outstandig debits total is correctly calculated' );
75 foreach my $line ( @{ $lines->as_list } ) {
76 my $fetched_line = Koha::Account::Lines->find( $generated_lines[$i]->id );
77 is_deeply( $line->unblessed, $fetched_line->unblessed, "Fetched line matches the generated one ($i)" );
78 is_deeply( $lines_arr[$i]->unblessed, $fetched_line->unblessed, "Fetched line matches the generated one ($i)" );
79 is( ref($lines_arr[$i]), 'Koha::Account::Line', 'outstanding_debits returns a list of Koha::Account::Line objects in list context' );
82 my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
83 Koha::Account::Line->new({ borrowernumber => $patron_2->id, amountoutstanding => -2, interface => 'commandline' })->store;
84 my $just_one = Koha::Account::Line->new({ borrowernumber => $patron_2->id, amount => 3, amountoutstanding => 3, interface => 'commandline' })->store;
85 Koha::Account::Line->new({ borrowernumber => $patron_2->id, amount => -6, amountoutstanding => -6, interface => 'commandline' })->store;
86 $lines = $patron_2->account->outstanding_debits();
87 is( $lines->total_outstanding, 3, "Total if some outstanding debits and some credits is only debits" );
88 is( $lines->count, 1, "With 1 outstanding debits, we get back a Lines object with 1 lines" );
89 my $the_line = Koha::Account::Lines->find( $just_one->id );
90 is_deeply( $the_line->unblessed, $lines->next->unblessed, "We get back the one correct line");
92 my $patron_3 = $builder->build_object({ class => 'Koha::Patrons' });
93 Koha::Account::Line->new({ borrowernumber => $patron_2->id, amount => -2, amountoutstanding => -2, interface => 'commandline' })->store;
94 Koha::Account::Line->new({ borrowernumber => $patron_2->id, amount => -20, amountoutstanding => -20, interface => 'commandline' })->store;
95 Koha::Account::Line->new({ borrowernumber => $patron_2->id, amount => -200, amountoutstanding => -200, interface => 'commandline' })->store;
96 $lines = $patron_3->account->outstanding_debits();
97 is( $lines->total_outstanding, 0, "Total if no outstanding debits total is 0" );
98 is( $lines->count, 0, "With 0 outstanding debits, we get back a Lines object with 0 lines" );
100 my $patron_4 = $builder->build_object({ class => 'Koha::Patrons' });
101 my $account_4 = $patron_4->account;
102 $lines = $account_4->outstanding_debits();
103 is( $lines->total_outstanding, 0, "Total if no outstanding debits is 0" );
104 is( $lines->count, 0, "With no outstanding debits, we get back a Lines object with 0 lines" );
106 # create a pathological credit with amountoutstanding > 0 (BZ 14591)
107 Koha::Account::Line->new({ borrowernumber => $patron_4->id, amount => -3, amountoutstanding => 3, interface => 'commandline' })->store();
108 $lines = $account_4->outstanding_debits();
109 is( $lines->count, 0, 'No credits are confused with debits because of the amountoutstanding value' );
111 $schema->storage->txn_rollback;
114 subtest 'outstanding_credits() tests' => sub {
118 $schema->storage->txn_begin;
120 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
121 my $account = $patron->account;
124 push @generated_lines, $account->add_credit({ amount => 1, interface => 'commandline' });
125 push @generated_lines, $account->add_credit({ amount => 2, interface => 'commandline' });
126 push @generated_lines, $account->add_credit({ amount => 3, interface => 'commandline' });
127 push @generated_lines, $account->add_credit({ amount => 4, interface => 'commandline' });
129 my $lines = $account->outstanding_credits();
130 my @lines_arr = $account->outstanding_credits();
132 is( ref($lines), 'Koha::Account::Lines', 'Called in scalar context, outstanding_credits returns a Koha::Account::Lines object' );
133 is( $lines->total_outstanding, -10, 'Outstandig credits total is correctly calculated' );
136 foreach my $line ( @{ $lines->as_list } ) {
137 my $fetched_line = Koha::Account::Lines->find( $generated_lines[$i]->id );
138 is_deeply( $line->unblessed, $fetched_line->unblessed, "Fetched line matches the generated one ($i)" );
139 is_deeply( $lines_arr[$i]->unblessed, $fetched_line->unblessed, "Fetched line matches the generated one ($i)" );
140 is( ref($lines_arr[$i]), 'Koha::Account::Line', 'outstanding_debits returns a list of Koha::Account::Line objects in list context' );
144 my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
145 $account = $patron_2->account;
146 $lines = $account->outstanding_credits();
147 is( $lines->total_outstanding, 0, "Total if no outstanding credits is 0" );
148 is( $lines->count, 0, "With no outstanding credits, we get back a Lines object with 0 lines" );
150 # create a pathological debit with amountoutstanding < 0 (BZ 14591)
151 Koha::Account::Line->new({ borrowernumber => $patron_2->id, amount => 2, amountoutstanding => -3, interface => 'commandline' })->store();
152 $lines = $account->outstanding_credits();
153 is( $lines->count, 0, 'No debits are confused with credits because of the amountoutstanding value' );
155 $schema->storage->txn_rollback;
158 subtest 'add_credit() tests' => sub {
162 $schema->storage->txn_begin;
164 # delete logs and statistics
165 my $action_logs = $schema->resultset('ActionLog')->search()->count;
166 my $statistics = $schema->resultset('Statistic')->search()->count;
168 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
169 my $account = Koha::Account->new( { patron_id => $patron->borrowernumber } );
171 is( $account->balance, 0, 'Patron has no balance' );
174 t::lib::Mocks::mock_preference( 'FinesLog', 0 );
176 my $line_1 = $account->add_credit(
178 description => 'Payment of 25',
179 library_id => $patron->branchcode,
180 note => 'not really important',
182 user_id => $patron->id,
183 interface => 'commandline'
187 is( $account->balance, -25, 'Patron has a balance of -25' );
188 is( $schema->resultset('ActionLog')->count(), $action_logs + 0, 'No log was added' );
189 is( $schema->resultset('Statistic')->count(), $statistics + 1, 'Action added to statistics' );
190 is( $line_1->accounttype, $Koha::Account::account_type_credit->{'payment'}, 'Account type is correctly set' );
193 t::lib::Mocks::mock_preference( 'FinesLog', 1 );
196 my $line_2 = $account->add_credit(
198 description => 'Payment of 37',
199 library_id => $patron->branchcode,
200 note => 'not really important',
201 user_id => $patron->id,
203 interface => 'commandline'
207 is( $account->balance, -62, 'Patron has a balance of -25' );
208 is( $schema->resultset('ActionLog')->count(), $action_logs + 1, 'Log was added' );
209 is( $schema->resultset('Statistic')->count(), $statistics + 2, 'Action added to statistics' );
210 is( $line_2->accounttype, $Koha::Account::account_type_credit->{'payment'} . $sip_code, 'Account type is correctly set' );
212 # offsets have the credit_id set to accountlines_id, and debit_id is undef
213 my $offset_1 = Koha::Account::Offsets->search({ credit_id => $line_1->id })->next;
214 my $offset_2 = Koha::Account::Offsets->search({ credit_id => $line_2->id })->next;
216 is( $offset_1->credit_id, $line_1->id, 'No debit_id is set for credits' );
217 is( $offset_1->debit_id, undef, 'No debit_id is set for credits' );
218 is( $offset_2->credit_id, $line_2->id, 'No debit_id is set for credits' );
219 is( $offset_2->debit_id, undef, 'No debit_id is set for credits' );
221 my $line_3 = $account->add_credit(
223 description => 'Manual credit applied',
224 library_id => $patron->branchcode,
225 user_id => $patron->id,
227 interface => 'commandline'
231 is( $schema->resultset('ActionLog')->count(), $action_logs + 2, 'Log was added' );
232 is( $schema->resultset('Statistic')->count(), $statistics + 2, 'No action added to statistics, because of credit type' );
234 $schema->storage->txn_rollback;
237 subtest 'add_debit() tests' => sub {
241 $schema->storage->txn_begin;
243 # delete logs and statistics
244 my $action_logs = $schema->resultset('ActionLog')->search()->count;
245 my $statistics = $schema->resultset('Statistic')->search()->count;
247 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
249 Koha::Account->new( { patron_id => $patron->borrowernumber } );
251 is( $account->balance, 0, 'Patron has no balance' );
257 description => 'amount validation failure',
258 library_id => $patron->branchcode,
259 note => 'this should fail anyway',
261 user_id => $patron->id,
262 interface => 'commandline'
264 ); } 'Koha::Exceptions::Account::AmountNotPositive', 'Expected validation exception thrown (amount)';
270 description => 'type validation failure',
271 library_id => $patron->branchcode,
272 note => 'this should fail anyway',
274 user_id => $patron->id,
275 interface => 'commandline'
277 ); } 'Koha::Exceptions::Account::UnrecognisedType', 'Expected validation exception thrown (type)';
280 t::lib::Mocks::mock_preference( 'FinesLog', 0 );
282 my $line_1 = $account->add_debit(
285 description => 'Rental charge of 25',
286 library_id => $patron->branchcode,
287 note => 'not really important',
289 user_id => $patron->id,
290 interface => 'commandline'
294 is( $account->balance, 25, 'Patron has a balance of 25' );
296 $schema->resultset('ActionLog')->count(),
301 $line_1->accounttype,
302 $Koha::Account::account_type_debit->{'rent'},
303 'Account type is correctly set'
307 t::lib::Mocks::mock_preference( 'FinesLog', 1 );
310 my $line_2 = $account->add_debit(
313 description => 'Rental charge of 37',
314 library_id => $patron->branchcode,
315 note => 'not really important',
317 user_id => $patron->id,
318 interface => 'commandline'
322 is( $account->balance, 62, 'Patron has a balance of 62' );
324 $schema->resultset('ActionLog')->count(),
329 $line_2->accounttype,
330 $Koha::Account::account_type_debit->{'rent'},
331 'Account type is correctly set'
334 # offsets have the debit_id set to accountlines_id, and credit_id is undef
336 Koha::Account::Offsets->search( { debit_id => $line_1->id } )->next;
338 Koha::Account::Offsets->search( { debit_id => $line_2->id } )->next;
340 is( $offset_1->debit_id, $line_1->id, 'debit_id is set for debit 1' );
341 is( $offset_1->credit_id, undef, 'credit_id is not set for debit 1' );
342 is( $offset_2->debit_id, $line_2->id, 'debit_id is set for debit 2' );
343 is( $offset_2->credit_id, undef, 'credit_id is not set for debit 2' );
345 $schema->storage->txn_rollback;
348 subtest 'lines() tests' => sub {
352 $schema->storage->txn_begin;
354 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
355 my $account = $patron->account;
360 push @generated_lines, $account->add_credit({ amount => 1, interface => 'commandline' });
361 push @generated_lines, $account->add_credit({ amount => 2, interface => 'commandline' });
362 push @generated_lines, $account->add_credit({ amount => 3, interface => 'commandline' });
363 push @generated_lines, $account->add_credit({ amount => 4, interface => 'commandline' });
366 push @generated_lines, Koha::Account::Line->new({ borrowernumber => $patron->id, amountoutstanding => 1, interface => 'commandline' })->store;
367 push @generated_lines, Koha::Account::Line->new({ borrowernumber => $patron->id, amountoutstanding => 2, interface => 'commandline' })->store;
368 push @generated_lines, Koha::Account::Line->new({ borrowernumber => $patron->id, amountoutstanding => 3, interface => 'commandline' })->store;
369 push @generated_lines, Koha::Account::Line->new({ borrowernumber => $patron->id, amountoutstanding => 4, interface => 'commandline' })->store;
372 push @generated_lines, Koha::Account::Line->new({ borrowernumber => $patron->id, amountoutstanding => 0, interface => 'commandline' })->store;
373 push @generated_lines, Koha::Account::Line->new({ borrowernumber => $patron->id, amountoutstanding => 0, interface => 'commandline' })->store;
375 my $lines = $account->lines;
376 is( $lines->_resultset->count, 10, "All accountlines (debits, credits and paid off) were fetched");
378 $schema->storage->txn_rollback;
381 subtest 'reconcile_balance' => sub {
385 subtest 'more credit than debit' => sub {
389 $schema->storage->txn_begin;
391 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
392 my $account = $patron->account;
395 $account->add_credit({ amount => 1, interface => 'commandline' });
396 $account->add_credit({ amount => 2, interface => 'commandline' });
397 $account->add_credit({ amount => 3, interface => 'commandline' });
398 $account->add_credit({ amount => 4, interface => 'commandline' });
399 $account->add_credit({ amount => 5, interface => 'commandline' });
401 # Add Debits TODO: replace for calls to add_debit when time comes
402 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 1, amountoutstanding => 1, interface => 'commandline' })->store;
403 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 2, amountoutstanding => 2, interface => 'commandline' })->store;
404 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 3, amountoutstanding => 3, interface => 'commandline' })->store;
405 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 4, amountoutstanding => 4, interface => 'commandline' })->store;
408 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 1, amountoutstanding => 0, interface => 'commandline' })->store;
409 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 1, amountoutstanding => 0, interface => 'commandline' })->store;
411 is( $account->balance(), -5, "Account balance is -5" );
412 is( $account->outstanding_debits->total_outstanding, 10, 'Outstanding debits sum 10' );
413 is( $account->outstanding_credits->total_outstanding, -15, 'Outstanding credits sum -15' );
415 $account->reconcile_balance();
417 is( $account->balance(), -5, "Account balance is -5" );
418 is( $account->outstanding_debits->total_outstanding, 0, 'No outstanding debits' );
419 is( $account->outstanding_credits->total_outstanding, -5, 'Outstanding credits sum -5' );
421 $schema->storage->txn_rollback;
424 subtest 'same debit as credit' => sub {
428 $schema->storage->txn_begin;
430 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
431 my $account = $patron->account;
434 $account->add_credit({ amount => 1, interface => 'commandline' });
435 $account->add_credit({ amount => 2, interface => 'commandline' });
436 $account->add_credit({ amount => 3, interface => 'commandline' });
437 $account->add_credit({ amount => 4, interface => 'commandline' });
439 # Add Debits TODO: replace for calls to add_debit when time comes
440 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 1, amountoutstanding => 1, interface => 'commandline' })->store;
441 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 2, amountoutstanding => 2, interface => 'commandline' })->store;
442 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 3, amountoutstanding => 3, interface => 'commandline' })->store;
443 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 4, amountoutstanding => 4, interface => 'commandline' })->store;
446 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 1, amountoutstanding => 0, interface => 'commandline' })->store;
447 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 1, amountoutstanding => 0, interface => 'commandline' })->store;
449 is( $account->balance(), 0, "Account balance is 0" );
450 is( $account->outstanding_debits->total_outstanding, 10, 'Outstanding debits sum 10' );
451 is( $account->outstanding_credits->total_outstanding, -10, 'Outstanding credits sum -10' );
453 $account->reconcile_balance();
455 is( $account->balance(), 0, "Account balance is 0" );
456 is( $account->outstanding_debits->total_outstanding, 0, 'No outstanding debits' );
457 is( $account->outstanding_credits->total_outstanding, 0, 'Outstanding credits sum 0' );
459 $schema->storage->txn_rollback;
462 subtest 'more debit than credit' => sub {
466 $schema->storage->txn_begin;
468 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
469 my $account = $patron->account;
472 $account->add_credit({ amount => 1, interface => 'commandline' });
473 $account->add_credit({ amount => 2, interface => 'commandline' });
474 $account->add_credit({ amount => 3, interface => 'commandline' });
475 $account->add_credit({ amount => 4, interface => 'commandline' });
477 # Add Debits TODO: replace for calls to add_debit when time comes
478 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 1, amountoutstanding => 1, interface => 'commandline' })->store;
479 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 2, amountoutstanding => 2, interface => 'commandline' })->store;
480 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 3, amountoutstanding => 3, interface => 'commandline' })->store;
481 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 4, amountoutstanding => 4, interface => 'commandline' })->store;
482 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 5, amountoutstanding => 5, interface => 'commandline' })->store;
485 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 1, amountoutstanding => 0, interface => 'commandline' })->store;
486 Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 1, amountoutstanding => 0, interface => 'commandline' })->store;
488 is( $account->balance(), 5, "Account balance is 5" );
489 is( $account->outstanding_debits->total_outstanding, 15, 'Outstanding debits sum 15' );
490 is( $account->outstanding_credits->total_outstanding, -10, 'Outstanding credits sum -10' );
492 $account->reconcile_balance();
494 is( $account->balance(), 5, "Account balance is 5" );
495 is( $account->outstanding_debits->total_outstanding, 5, 'Outstanding debits sum 5' );
496 is( $account->outstanding_credits->total_outstanding, 0, 'Outstanding credits sum 0' );
498 $schema->storage->txn_rollback;
501 subtest 'credits are applied to older debits first' => sub {
505 $schema->storage->txn_begin;
507 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
508 my $account = $patron->account;
511 $account->add_credit({ amount => 1, interface => 'commandline' });
512 $account->add_credit({ amount => 3, interface => 'commandline' });
514 # Add Debits TODO: replace for calls to add_debit when time comes
515 my $debit_1 = Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 1, amountoutstanding => 1, interface => 'commandline' })->store;
516 my $debit_2 = Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 2, amountoutstanding => 2, interface => 'commandline' })->store;
517 my $debit_3 = Koha::Account::Line->new({ borrowernumber => $patron->id, amount => 3, amountoutstanding => 3, interface => 'commandline' })->store;
519 is( $account->balance(), 2, "Account balance is 2" );
520 is( $account->outstanding_debits->total_outstanding, 6, 'Outstanding debits sum 6' );
521 is( $account->outstanding_credits->total_outstanding, -4, 'Outstanding credits sum -4' );
523 $account->reconcile_balance();
525 is( $account->balance(), 2, "Account balance is 2" );
526 is( $account->outstanding_debits->total_outstanding, 2, 'Outstanding debits sum 2' );
527 is( $account->outstanding_credits->total_outstanding, 0, 'Outstanding credits sum 0' );
529 $debit_1->discard_changes;
530 is( $debit_1->amountoutstanding + 0, 0, 'Old debit payed' );
531 $debit_2->discard_changes;
532 is( $debit_2->amountoutstanding + 0, 0, 'Old debit payed' );
533 $debit_3->discard_changes;
534 is( $debit_3->amountoutstanding + 0, 2, 'Newest debit only partially payed' );
536 $schema->storage->txn_rollback;
540 subtest 'pay() tests' => sub {
544 $schema->storage->txn_begin;
546 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
547 my $library = $builder->build_object({ class => 'Koha::Libraries' });
548 my $account = $patron->account;
550 my $context = Test::MockModule->new('C4::Context');
551 $context->mock( 'userenv', { branch => $library->id } );
553 my $credit_1_id = $account->pay({ amount => 200 });
554 my $credit_1 = Koha::Account::Lines->find( $credit_1_id );
556 is( $credit_1->branchcode, undef, 'No branchcode is set if library_id was not passed' );
558 my $credit_2_id = $account->pay({ amount => 150, library_id => $library->id });
559 my $credit_2 = Koha::Account::Lines->find( $credit_2_id );
561 is( $credit_2->branchcode, $library->id, 'branchcode set because library_id was passed' );
563 $schema->storage->txn_rollback;