4 use Test::More tests => 17;
12 use t::lib::TestBuilder;
14 use_ok('C4::Overdues', qw( GetOverdueMessageTransportTypes GetBranchcodesWithOverdueRules UpdateFine ));
15 can_ok('C4::Overdues', 'GetOverdueMessageTransportTypes');
16 can_ok('C4::Overdues', 'GetBranchcodesWithOverdueRules');
18 my $schema = Koha::Database->new->schema;
19 my $builder = t::lib::TestBuilder->new;
21 $schema->storage->txn_begin;
22 my $dbh = C4::Context->dbh;
24 $dbh->do(q|DELETE FROM letter|);
25 $dbh->do(q|DELETE FROM message_queue|);
26 $dbh->do(q|DELETE FROM message_transport_types|);
27 $dbh->do(q|DELETE FROM overduerules|);
28 $dbh->do(q|DELETE FROM overduerules_transport_types|);
31 INSERT INTO message_transport_types( message_transport_type ) VALUES ('email'), ('phone'), ('print'), ('sms')
35 INSERT INTO overduerules ( overduerules_id, branchcode, categorycode ) VALUES
42 $dbh->do(q|INSERT INTO overduerules_transport_types (overduerules_id, letternumber, message_transport_type) VALUES
58 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL', 'PT');
59 is( $mtts, undef, 'GetOverdueMessageTransportTypes: returns undef if no letternumber given' );
61 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL', undef, 1);
62 is( $mtts, undef, 'GetOverdueMessageTransportTypes: returns undef if no categorycode given' );
64 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL');
65 is( $mtts, undef, 'GetOverdueMessageTransportTypes: returns undef if no letternumber and categorycode given' );
67 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL', 'PT', 1);
68 is_deeply( $mtts, ['email'], 'GetOverdueMessageTransportTypes: first overdue is by email for PT (CPL)' );
70 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL', 'PT', 2);
71 is_deeply( $mtts, ['sms'], 'GetOverdueMessageTransportTypes: second overdue is by sms for PT (CPL)' );
73 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL', 'PT', 3);
74 is_deeply( $mtts, ['email'], 'GetOverdueMessageTransportTypes: third overdue is by email for PT (CPL)' );
76 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('', 'PT', 1);
77 is_deeply( $mtts, ['email'], 'GetOverdueMessageTransportTypes: first overdue is by email for PT (default)' );
79 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('', 'PT', 2);
80 is_deeply( $mtts, ['email', 'sms'], 'GetOverdueMessageTransportTypes: second overdue is by email and sms for PT (default)' );
82 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('', 'PT', 3);
83 is_deeply( $mtts, ['print', 'sms', 'email'], 'GetOverdueMessageTransportTypes: third overdue is by print, sms and email for PT (default). With print in first.' );
85 # Test GetBranchcodesWithOverdueRules
86 $dbh->do(q|DELETE FROM overduerules|);
88 INSERT INTO overduerules
89 ( branchcode,categorycode, delay1,letter1,debarred1, delay2,letter2,debarred2, delay3,letter3,debarred3 )
91 ( '', '', 1, 'LETTER_CODE1', 1, 5, 'LETTER_CODE2', 1, 10, 'LETTER_CODE3', 1 )
94 my @branchcodes = map { $_->branchcode } Koha::Libraries->search;
96 my @overdue_branches = C4::Overdues::GetBranchcodesWithOverdueRules();
97 is_deeply( [ sort @overdue_branches ], [ sort @branchcodes ], 'If a default rule exists, all branches should be returned' );
100 INSERT INTO overduerules
101 ( branchcode,categorycode, delay1,letter1,debarred1, delay2,letter2,debarred2, delay3,letter3,debarred3 )
103 ( 'CPL', '', 1, 'LETTER_CODE1', 1, 5, 'LETTER_CODE2', 1, 10, 'LETTER_CODE3', 1 )
106 @overdue_branches = C4::Overdues::GetBranchcodesWithOverdueRules();
107 is_deeply( [ sort @overdue_branches ], [ sort @branchcodes ], 'If a default rule exists and a specific rule exists, all branches should be returned' );
109 $dbh->do(q|DELETE FROM overduerules|);
111 INSERT INTO overduerules
112 ( branchcode,categorycode, delay1,letter1,debarred1, delay2,letter2,debarred2, delay3,letter3,debarred3 )
114 ( 'CPL', '', 1, 'LETTER_CODE1', 1, 5, 'LETTER_CODE2', 1, 10, 'LETTER_CODE3', 1 )
117 @overdue_branches = C4::Overdues::GetBranchcodesWithOverdueRules();
118 is_deeply( \@overdue_branches, ['CPL'] , 'If only a specific rule exist, only 1 branch should be returned' );
120 $dbh->do(q|DELETE FROM overduerules|);
122 INSERT INTO overduerules
123 ( branchcode,categorycode, delay1,letter1,debarred1, delay2,letter2,debarred2, delay3,letter3,debarred3 )
125 ( 'CPL', '', 1, 'LETTER_CODE1_CPL', 1, 5, 'LETTER_CODE2_CPL', 1, 10, 'LETTER_CODE3_CPL', 1 ),
126 ( 'MPL', '', 1, 'LETTER_CODE1_MPL', 1, 5, 'LETTER_CODE2_MPL', 1, 10, 'LETTER_CODE3_MPL', 1 )
129 @overdue_branches = C4::Overdues::GetBranchcodesWithOverdueRules();
130 is_deeply( \@overdue_branches, ['CPL', 'MPL'] , 'If only 2 specific rules exist, 2 branches should be returned' );
132 $schema->storage->txn_rollback;
134 subtest 'UpdateFine tests' => sub {
138 $schema->storage->txn_begin;
140 t::lib::Mocks::mock_preference( 'MaxFine', '100' );
142 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
143 my $item1 = $builder->build_sample_item();
144 my $item2 = $builder->build_sample_item();
145 my $checkout1 = $builder->build_object(
147 class => 'Koha::Checkouts',
148 value => { itemnumber => $item1->itemnumber, borrowernumber => $patron->id }
151 my $checkout2 = $builder->build_object(
153 class => 'Koha::Checkouts',
154 value => { itemnumber => $item2->itemnumber, borrowernumber => $patron->id }
158 # Try to add 0 amount fine
161 issue_id => $checkout1->issue_id,
162 itemnumber => $item1->itemnumber,
163 borrowernumber => $patron->borrowernumber,
165 due => $checkout1->date_due
169 my $fines = Koha::Account::Lines->search(
170 { borrowernumber => $patron->borrowernumber } );
171 is( $fines->count, 0, "No fine added when amount is 0" );
172 # Total : Outstanding : MaxFine
175 # Add fine 1 - First Item Overdue
178 issue_id => $checkout1->issue_id,
179 itemnumber => $item1->itemnumber,
180 borrowernumber => $patron->borrowernumber,
182 due => $checkout1->date_due
186 $fines = Koha::Account::Lines->search(
187 { borrowernumber => $patron->borrowernumber } );
188 is( $fines->count, 1, "Fine added when amount is greater than 0" );
189 my $fine = $fines->next;
190 is( $fine->amount+0, 50, "Fine amount correctly set to 50" );
191 is( $fine->amountoutstanding+0, 50, "Fine amountoutstanding correctly set to 50" );
192 is( $fine->issue_id, $checkout1->issue_id, "Fine is associated with the correct issue" );
193 is( $fine->itemnumber, $checkout1->itemnumber, "Fine is associated with the correct item" );
194 # Total : Outstanding : MaxFine
197 # Increase fine 1 - First Item Overdue
200 issue_id => $checkout1->issue_id,
201 itemnumber => $item1->itemnumber,
202 borrowernumber => $patron->borrowernumber,
204 due => $checkout1->date_due
208 $fines = Koha::Account::Lines->search(
209 { borrowernumber => $patron->borrowernumber } );
210 is( $fines->count, 1, "Existing fine updated" );
211 $fine = $fines->next;
212 is( $fine->amount+0, 80, "Fine amount correctly updated to 80" );
213 is( $fine->amountoutstanding+0, 80, "Fine amountoutstanding correctly updated to 80" );
214 # Total : Outstanding : MaxFine
217 # Add fine 2 - Second Item Overdue
220 issue_id => $checkout2->issue_id,
221 itemnumber => $item2->itemnumber,
222 borrowernumber => $patron->borrowernumber,
224 due => $checkout2->date_due
228 $fines = Koha::Account::Lines->search(
229 { borrowernumber => $patron->borrowernumber },
230 { order_by => { '-asc' => 'accountlines_id' } }
232 is( $fines->count, 2, "New fine added for second checkout" );
233 $fine = $fines->next;
234 is( $fine->amount+0, 80, "First fine amount unchanged" );
235 is( $fine->amountoutstanding+0, 80, "First fine amountoutstanding unchanged" );
236 my $fine2 = $fines->next;
237 is( $fine2->amount+0, 20, "Second fine capped at '20' by MaxFine" );
238 is( $fine2->amountoutstanding+0, 20, "Second fine amountoutstanding capped at '20' by MaxFine" );
239 is( $fine2->issue_id, $checkout2->issue_id, "Second fine is associated with the correct issue" );
240 is( $fine2->itemnumber, $checkout2->itemnumber, "Second fine is associated with the correct item" );
241 is( $fine->amount + $fine2->amount, '100', "Total fines = 100" );
242 is( $fine->amountoutstanding + $fine2->amountoutstanding, '100', "Total outstanding = 100" );
243 # Total : Outstanding : MaxFine
246 # A day passes, the item is still overdue, update fine is called again
247 # we don't expect to increase above MaxFine of 100
250 issue_id => $checkout2->issue_id,
251 itemnumber => $item2->itemnumber,
252 borrowernumber => $patron->borrowernumber,
254 due => $checkout2->date_due
258 $fines = Koha::Account::Lines->search(
259 { borrowernumber => $patron->borrowernumber },
260 { order_by => { '-asc' => 'accountlines_id' } }
262 is( $fines->count, 2, "Existing fine updated for second checkout, no new fine added" );
263 $fine = $fines->next;
264 is( $fine->amount+0, 80, "First fine amount unchanged" );
265 is( $fine->amountoutstanding+0, 80, "First fine amountoutstanding unchanged" );
266 $fine2 = $fines->next;
267 is( $fine2->amount+0, 20, "Second fine capped at '20' by MaxFine" );
268 is( $fine2->amountoutstanding+0, 20, "Second fine amountoutstanding capped at '20' by MaxFine" );
269 is( $fine2->issue_id, $checkout2->issue_id, "Second fine is associated with the correct issue" );
270 is( $fine2->itemnumber, $checkout2->itemnumber, "Second fine is associated with the correct item" );
271 is( $fine->amount + $fine2->amount, '100', "Total fines = 100" );
272 is( $fine->amountoutstanding + $fine2->amountoutstanding, '100', "Total outstanding = 100" );
273 # Total : Outstanding : MaxFine
277 $fine->amountoutstanding(50)->store;
278 # Total : Outstanding : MaxFine
281 # Increase fine 2 - Second item overdue
284 issue_id => $checkout2->issue_id,
285 itemnumber => $item2->itemnumber,
286 borrowernumber => $patron->borrowernumber,
288 due => $checkout2->date_due
292 $fines = Koha::Account::Lines->search(
293 { borrowernumber => $patron->borrowernumber },
294 { order_by => { '-asc' => 'accountlines_id' } }
296 is( $fines->count, 2, "Still two fines after second checkout update" );
297 $fine = $fines->next;
298 is( $fine->amount+0, 80, "First fine amount unchanged" );
299 is( $fine->amountoutstanding+0, 50, "First fine amountoutstanding unchanged" );
300 $fine2 = $fines->next;
301 is( $fine2->amount+0, 30, "Second fine increased after partial payment of first" );
302 is( $fine2->amountoutstanding+0, 30, "Second fine amountoutstanding increased after partial payment of first" );
303 is( $fine->amount + $fine2->amount, '110', "Total fines = 100" );
304 is( $fine->amountoutstanding + $fine2->amountoutstanding, '80', "Total outstanding = 80" );
305 # Total : Outstanding : MaxFine
308 # Fix fine 1 - First item renewed
309 $fine->status('RETURNED')->store;
311 # Add fine 3 - First item second overdue
314 issue_id => $checkout1->issue_id,
315 itemnumber => $item1->itemnumber,
316 borrowernumber => $patron->borrowernumber,
318 due => $checkout1->date_due
322 $fines = Koha::Account::Lines->search(
323 { borrowernumber => $patron->borrowernumber },
324 { order_by => { '-asc' => 'accountlines_id' } }
326 is( $fines->count, 3, "Third fine added for overdue renewal" );
327 $fine = $fines->next;
328 is( $fine->amount+0, 80, "First fine amount unchanged" );
329 is( $fine->amountoutstanding+0, 50, "First fine amountoutstanding unchanged" );
330 $fine2 = $fines->next;
331 is( $fine2->amount+0, 30, "Second fine amount unchanged" );
332 is( $fine2->amountoutstanding+0, 30, "Second fine amountoutstanding unchanged" );
333 my $fine3 = $fines->next;
334 is( $fine3->amount+0, 20, "Third fine amount capped due to MaxFine" );
335 is( $fine3->amountoutstanding+0, 20, "Third fine amountoutstanding capped at '20' by MaxFine" );
336 is( $fine3->issue_id, $checkout1->issue_id, "Third fine is associated with the correct issue" );
337 is( $fine3->itemnumber, $checkout1->itemnumber, "Third fine is associated with the correct item" );
338 is( $fine->amount + $fine2->amount + $fine3->amount, '130', "Total fines = 130" );
339 is( $fine->amountoutstanding + $fine2->amountoutstanding + $fine3->amountoutstanding, '100', "Total outstanding = 100" );
340 # Total : Outstanding : MaxFine
343 # Payoff accruing fine and ensure next increment doesn't create a new one (bug #24146)
344 $fine3->amountoutstanding('0')->store;
345 is( $fine->amount + $fine2->amount + $fine3->amount, '130', "Total fines = 130" );
346 is( $fine->amountoutstanding + $fine2->amountoutstanding + $fine3->amountoutstanding, '80', "Total outstanding = 80" );
347 # Total : Outstanding : MaxFine
350 # Increase fine 3 - First item, second overdue increase
353 issue_id => $checkout1->issue_id,
354 itemnumber => $item1->itemnumber,
355 borrowernumber => $patron->borrowernumber,
357 due => $checkout1->date_due
361 $fines = Koha::Account::Lines->search(
362 { borrowernumber => $patron->borrowernumber },
363 { order_by => { '-asc' => 'accountlines_id' } }
365 is( $fines->count, 3, "Still three fines after third checkout update" );
366 $fine = $fines->next;
367 is( $fine->amount+0, 80, "First fine amount unchanged" );
368 is( $fine->amountoutstanding+0, 50, "First fine amountoutstanding unchanged" );
369 $fine2 = $fines->next;
370 is( $fine2->amount+0, 30, "Second fine amount unchanged" );
371 is( $fine2->amountoutstanding+0, 30, "Second fine amountoutstanding unchanged" );
372 $fine3 = $fines->next;
373 is( $fine3->amount+0, 40, "Third fine amount capped due to MaxFine" );
374 is( $fine3->amountoutstanding+0, 20, "Third fine amountoutstanding increased ..." );
375 is( $fine3->issue_id, $checkout1->issue_id, "Third fine is associated with the correct issue" );
376 is( $fine3->itemnumber, $checkout1->itemnumber, "Third fine is associated with the correct item" );
377 is( $fine->amount + $fine2->amount + $fine3->amount, '150', "Total fines = 150" );
378 is( $fine->amountoutstanding + $fine2->amountoutstanding + $fine3->amountoutstanding, '100', "Total outstanding = 100" );
379 # Total : Outstanding : MaxFine
382 # FIXME: Add test to check whether sundry/manual charges are included within MaxFine.
383 # FIXME: Add test to ensure other charges are not included within MaxFine.
386 t::lib::Mocks::mock_preference( 'MaxFine', '0' );
389 issue_id => $checkout1->issue_id,
390 itemnumber => $item1->itemnumber,
391 borrowernumber => $patron->borrowernumber,
393 due => $checkout1->date_due
397 $fines = Koha::Account::Lines->search(
398 { borrowernumber => $patron->borrowernumber },
399 { order_by => { '-asc' => 'accountlines_id' } }
401 is( $fines->count, 3, "Still only three fines after MaxFine cap removed" );
402 $fine = $fines->next;
403 is( $fine->amount+0, 80, "First fine amount unchanged" );
404 $fine2 = $fines->next;
405 is( $fine2->amount+0, 30, "Second fine amount unchanged" );
406 $fine3 = $fines->next;
407 is( $fine3->amount+0, 50, "Third fine increased now MaxFine cap is disabled" );
408 is( $fine3->amountoutstanding+0, 30, "Third fine increased now MaxFine cap is disabled" );
410 # If somehow the fine should be reduced, we changed rules or checkout date or something
413 issue_id => $checkout1->issue_id,
414 itemnumber => $item1->itemnumber,
415 borrowernumber => $patron->borrowernumber,
417 due => $checkout1->date_due
421 $fines = Koha::Account::Lines->search(
422 { borrowernumber => $patron->borrowernumber },
423 { order_by => { '-asc' => 'accountlines_id' } }
425 is( $fines->count, 3, "Still only three fines after MaxFine cap removed and third fine altered" );
426 $fine = $fines->next;
427 is( $fine->amount+0, 80, "First fine amount unchanged" );
428 $fine2 = $fines->next;
429 is( $fine2->amount+0, 30, "Second fine amount unchanged" );
430 $fine3 = $fines->next;
431 is( $fine3->amount+0, 30, "Third fine reduced" );
432 is( $fine3->amountoutstanding+0, 10, "Third fine amount outstanding is reduced" );
434 # Ensure maxfine calculations work correctly for floats (bug #25127)
435 # 7.2 (maxfine) - 7.2 (total_amount_other) != 8.88178419700125e-16 (😢)
436 t::lib::Mocks::mock_preference( 'MaxFine', '7.2' );
437 my $patron_1 = $builder->build_object( { class => 'Koha::Patrons' } );
438 my $item_1 = $builder->build_sample_item();
439 my $item_2 = $builder->build_sample_item();
440 my $checkout_1 = $builder->build_object(
442 class => 'Koha::Checkouts',
444 itemnumber => $item_1->itemnumber,
445 borrowernumber => $patron_1->id
449 my $checkout_2 = $builder->build_object(
451 class => 'Koha::Checkouts',
453 itemnumber => $item_2->itemnumber,
454 borrowernumber => $patron->id
458 my $account = $patron_1->account;
463 issue_id => $checkout_1->issue_id,
471 issue_id => $checkout_1->issue_id,
479 issue_id => $checkout_1->issue_id,
487 issue_id => $checkout_1->issue_id,
493 issue_id => $checkout_2->issue_id,
494 itemnumber => $item_2->itemnumber,
495 borrowernumber => $patron_1->borrowernumber,
497 due => $checkout_2->date_due
500 $fines = Koha::Account::Lines->search(
501 { borrowernumber => $patron_1->borrowernumber },
502 { order_by => { '-asc' => 'accountlines_id' } }
504 is( $fines->count, 4, "New amount should be 0 so no fine added" );
505 ok( C4::Circulation::AddReturn( $item_1->barcode, $item_1->homebranch, 1), "Returning the item and forgiving fines succeeds");
507 t::lib::Mocks::mock_preference( 'MaxFine', 0 );
509 # Ensure CalcFine calculations work correctly for floats (bug #27079)
510 # 1.800000 (amount from database) != 1.8~ (CalcFine of 0.15cents * 12units) (😢)
511 my $amount = 0.15 * 12;
514 issue_id => $checkout_2->issue_id,
515 itemnumber => $item_2->itemnumber,
516 borrowernumber => $patron_1->borrowernumber,
518 due => $checkout_2->date_due
521 $fine = Koha::Account::Lines->search({ issue_id => $checkout_2->issue_id })->single;
522 ok( $fine, 'Fine added for checkout 2');
523 is( $fine->amount, "1.800000", "Fine amount is 1.800000 as expected");
525 $fine->amountoutstanding(0)->store;
526 $fine->discard_changes;
527 is( $fine->amountoutstanding + 0, 0, "Fine was paid off");
530 issue_id => $checkout_2->issue_id,
531 itemnumber => $item_2->itemnumber,
532 borrowernumber => $patron_1->borrowernumber,
534 due => $checkout_2->date_due
537 my $refunds = Koha::Account::Lines->search({ itemnumber => $item_2->itemnumber, credit_type_code => 'OVERPAYMENT' });
538 is( $refunds->count, 0, "Overpayment refund not added when the amounts are equal" );
540 # Adding an OVERDUE fine not linked with a checkout (possible with historical OVERDUE fines)
541 $builder->build_object(
543 class => "Koha::Account::Lines",
545 borrowernumber => $patron_1->borrowernumber,
547 debit_type_code => 'OVERDUE',
551 $fine->issue_id(undef)->store;
555 issue_id => $checkout_2->issue_id,
556 itemnumber => $item_2->itemnumber,
557 borrowernumber => $patron_1->borrowernumber,
559 due => $checkout_2->date_due
562 } [], 'No warning generated if fine is not linked with a checkout';
564 $schema->storage->txn_rollback;