4 use Test::More tests => 17;
11 use t::lib::TestBuilder;
13 use_ok('C4::Overdues');
14 can_ok('C4::Overdues', 'GetOverdueMessageTransportTypes');
15 can_ok('C4::Overdues', 'GetBranchcodesWithOverdueRules');
17 my $schema = Koha::Database->new->schema;
18 my $builder = t::lib::TestBuilder->new;
20 $schema->storage->txn_begin;
21 my $dbh = C4::Context->dbh;
23 $dbh->do(q|DELETE FROM letter|);
24 $dbh->do(q|DELETE FROM message_queue|);
25 $dbh->do(q|DELETE FROM message_transport_types|);
26 $dbh->do(q|DELETE FROM overduerules|);
27 $dbh->do(q|DELETE FROM overduerules_transport_types|);
30 INSERT INTO message_transport_types( message_transport_type ) VALUES ('email'), ('phone'), ('print'), ('sms')
34 INSERT INTO overduerules ( overduerules_id, branchcode, categorycode ) VALUES
41 $dbh->do(q|INSERT INTO overduerules_transport_types (overduerules_id, letternumber, message_transport_type) VALUES
57 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL', 'PT');
58 is( $mtts, undef, 'GetOverdueMessageTransportTypes: returns undef if no letternumber given' );
60 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL', undef, 1);
61 is( $mtts, undef, 'GetOverdueMessageTransportTypes: returns undef if no categorycode given' );
63 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL');
64 is( $mtts, undef, 'GetOverdueMessageTransportTypes: returns undef if no letternumber and categorycode given' );
66 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL', 'PT', 1);
67 is_deeply( $mtts, ['email'], 'GetOverdueMessageTransportTypes: first overdue is by email for PT (CPL)' );
69 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL', 'PT', 2);
70 is_deeply( $mtts, ['sms'], 'GetOverdueMessageTransportTypes: second overdue is by sms for PT (CPL)' );
72 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL', 'PT', 3);
73 is_deeply( $mtts, ['email'], 'GetOverdueMessageTransportTypes: third overdue is by email for PT (CPL)' );
75 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('', 'PT', 1);
76 is_deeply( $mtts, ['email'], 'GetOverdueMessageTransportTypes: first overdue is by email for PT (default)' );
78 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('', 'PT', 2);
79 is_deeply( $mtts, ['email', 'sms'], 'GetOverdueMessageTransportTypes: second overdue is by email and sms for PT (default)' );
81 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('', 'PT', 3);
82 is_deeply( $mtts, ['print', 'sms', 'email'], 'GetOverdueMessageTransportTypes: third overdue is by print, sms and email for PT (default). With print in first.' );
84 # Test GetBranchcodesWithOverdueRules
85 $dbh->do(q|DELETE FROM overduerules|);
87 INSERT INTO overduerules
88 ( branchcode,categorycode, delay1,letter1,debarred1, delay2,letter2,debarred2, delay3,letter3,debarred3 )
90 ( '', '', 1, 'LETTER_CODE1', 1, 5, 'LETTER_CODE2', 1, 10, 'LETTER_CODE3', 1 )
93 my @branchcodes = map { $_->branchcode } Koha::Libraries->search;
95 my @overdue_branches = C4::Overdues::GetBranchcodesWithOverdueRules();
96 is_deeply( [ sort @overdue_branches ], [ sort @branchcodes ], 'If a default rule exists, all branches should be returned' );
99 INSERT INTO overduerules
100 ( branchcode,categorycode, delay1,letter1,debarred1, delay2,letter2,debarred2, delay3,letter3,debarred3 )
102 ( 'CPL', '', 1, 'LETTER_CODE1', 1, 5, 'LETTER_CODE2', 1, 10, 'LETTER_CODE3', 1 )
105 @overdue_branches = C4::Overdues::GetBranchcodesWithOverdueRules();
106 is_deeply( [ sort @overdue_branches ], [ sort @branchcodes ], 'If a default rule exists and a specific rule exists, all branches should be returned' );
108 $dbh->do(q|DELETE FROM overduerules|);
110 INSERT INTO overduerules
111 ( branchcode,categorycode, delay1,letter1,debarred1, delay2,letter2,debarred2, delay3,letter3,debarred3 )
113 ( 'CPL', '', 1, 'LETTER_CODE1', 1, 5, 'LETTER_CODE2', 1, 10, 'LETTER_CODE3', 1 )
116 @overdue_branches = C4::Overdues::GetBranchcodesWithOverdueRules();
117 is_deeply( \@overdue_branches, ['CPL'] , 'If only a specific rule exist, only 1 branch should be returned' );
119 $dbh->do(q|DELETE FROM overduerules|);
121 INSERT INTO overduerules
122 ( branchcode,categorycode, delay1,letter1,debarred1, delay2,letter2,debarred2, delay3,letter3,debarred3 )
124 ( 'CPL', '', 1, 'LETTER_CODE1_CPL', 1, 5, 'LETTER_CODE2_CPL', 1, 10, 'LETTER_CODE3_CPL', 1 ),
125 ( 'MPL', '', 1, 'LETTER_CODE1_MPL', 1, 5, 'LETTER_CODE2_MPL', 1, 10, 'LETTER_CODE3_MPL', 1 )
128 @overdue_branches = C4::Overdues::GetBranchcodesWithOverdueRules();
129 is_deeply( \@overdue_branches, ['CPL', 'MPL'] , 'If only 2 specific rules exist, 2 branches should be returned' );
131 $schema->storage->txn_rollback;
133 subtest 'UpdateFine tests' => sub {
137 $schema->storage->txn_begin;
139 t::lib::Mocks::mock_preference( 'MaxFine', '100' );
141 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
142 my $item1 = $builder->build_sample_item();
143 my $item2 = $builder->build_sample_item();
144 my $checkout1 = $builder->build_object(
146 class => 'Koha::Checkouts',
147 value => { itemnumber => $item1->itemnumber, borrowernumber => $patron->id }
150 my $checkout2 = $builder->build_object(
152 class => 'Koha::Checkouts',
153 value => { itemnumber => $item2->itemnumber, borrowernumber => $patron->id }
157 # Try to add 0 amount fine
160 issue_id => $checkout1->issue_id,
161 itemnumber => $item1->itemnumber,
162 borrowernumber => $patron->borrowernumber,
164 due => $checkout1->date_due
168 my $fines = Koha::Account::Lines->search(
169 { borrowernumber => $patron->borrowernumber } );
170 is( $fines->count, 0, "No fine added when amount is 0" );
171 # Total : Outstanding : MaxFine
174 # Add fine 1 - First Item Overdue
177 issue_id => $checkout1->issue_id,
178 itemnumber => $item1->itemnumber,
179 borrowernumber => $patron->borrowernumber,
181 due => $checkout1->date_due
185 $fines = Koha::Account::Lines->search(
186 { borrowernumber => $patron->borrowernumber } );
187 is( $fines->count, 1, "Fine added when amount is greater than 0" );
188 my $fine = $fines->next;
189 is( $fine->amount+0, 50, "Fine amount correctly set to 50" );
190 is( $fine->amountoutstanding+0, 50, "Fine amountoutstanding correctly set to 50" );
191 is( $fine->issue_id, $checkout1->issue_id, "Fine is associated with the correct issue" );
192 is( $fine->itemnumber, $checkout1->itemnumber, "Fine is associated with the correct item" );
193 # Total : Outstanding : MaxFine
196 # Increase fine 1 - First Item Overdue
199 issue_id => $checkout1->issue_id,
200 itemnumber => $item1->itemnumber,
201 borrowernumber => $patron->borrowernumber,
203 due => $checkout1->date_due
207 $fines = Koha::Account::Lines->search(
208 { borrowernumber => $patron->borrowernumber } );
209 is( $fines->count, 1, "Existing fine updated" );
210 $fine = $fines->next;
211 is( $fine->amount+0, 80, "Fine amount correctly updated to 80" );
212 is( $fine->amountoutstanding+0, 80, "Fine amountoutstanding correctly updated to 80" );
213 # Total : Outstanding : MaxFine
216 # Add fine 2 - Second Item Overdue
219 issue_id => $checkout2->issue_id,
220 itemnumber => $item2->itemnumber,
221 borrowernumber => $patron->borrowernumber,
223 due => $checkout2->date_due
227 $fines = Koha::Account::Lines->search(
228 { borrowernumber => $patron->borrowernumber },
229 { order_by => { '-asc' => 'accountlines_id' } }
231 is( $fines->count, 2, "New fine added for second checkout" );
232 $fine = $fines->next;
233 is( $fine->amount+0, 80, "First fine amount unchanged" );
234 is( $fine->amountoutstanding+0, 80, "First fine amountoutstanding unchanged" );
235 my $fine2 = $fines->next;
236 is( $fine2->amount+0, 20, "Second fine capped at '20' by MaxFine" );
237 is( $fine2->amountoutstanding+0, 20, "Second fine amountoutstanding capped at '20' by MaxFine" );
238 is( $fine2->issue_id, $checkout2->issue_id, "Second fine is associated with the correct issue" );
239 is( $fine2->itemnumber, $checkout2->itemnumber, "Second fine is associated with the correct item" );
240 is( $fine->amount + $fine2->amount, '100', "Total fines = 100" );
241 is( $fine->amountoutstanding + $fine2->amountoutstanding, '100', "Total outstanding = 100" );
242 # Total : Outstanding : MaxFine
245 # A day passes, the item is still overdue, update fine is called again
246 # we don't expect to increase above MaxFine of 100
249 issue_id => $checkout2->issue_id,
250 itemnumber => $item2->itemnumber,
251 borrowernumber => $patron->borrowernumber,
253 due => $checkout2->date_due
257 $fines = Koha::Account::Lines->search(
258 { borrowernumber => $patron->borrowernumber },
259 { order_by => { '-asc' => 'accountlines_id' } }
261 is( $fines->count, 2, "Existing fine updated for second checkout, no new fine added" );
262 $fine = $fines->next;
263 is( $fine->amount+0, 80, "First fine amount unchanged" );
264 is( $fine->amountoutstanding+0, 80, "First fine amountoutstanding unchanged" );
265 $fine2 = $fines->next;
266 is( $fine2->amount+0, 20, "Second fine capped at '20' by MaxFine" );
267 is( $fine2->amountoutstanding+0, 20, "Second fine amountoutstanding capped at '20' by MaxFine" );
268 is( $fine2->issue_id, $checkout2->issue_id, "Second fine is associated with the correct issue" );
269 is( $fine2->itemnumber, $checkout2->itemnumber, "Second fine is associated with the correct item" );
270 is( $fine->amount + $fine2->amount, '100', "Total fines = 100" );
271 is( $fine->amountoutstanding + $fine2->amountoutstanding, '100', "Total outstanding = 100" );
272 # Total : Outstanding : MaxFine
276 $fine->amountoutstanding(50)->store;
277 # Total : Outstanding : MaxFine
280 # Increase fine 2 - Second item overdue
283 issue_id => $checkout2->issue_id,
284 itemnumber => $item2->itemnumber,
285 borrowernumber => $patron->borrowernumber,
287 due => $checkout2->date_due
291 $fines = Koha::Account::Lines->search(
292 { borrowernumber => $patron->borrowernumber },
293 { order_by => { '-asc' => 'accountlines_id' } }
295 is( $fines->count, 2, "Still two fines after second checkout update" );
296 $fine = $fines->next;
297 is( $fine->amount+0, 80, "First fine amount unchanged" );
298 is( $fine->amountoutstanding+0, 50, "First fine amountoutstanding unchanged" );
299 $fine2 = $fines->next;
300 is( $fine2->amount+0, 30, "Second fine increased after partial payment of first" );
301 is( $fine2->amountoutstanding+0, 30, "Second fine amountoutstanding increased after partial payment of first" );
302 is( $fine->amount + $fine2->amount, '110', "Total fines = 100" );
303 is( $fine->amountoutstanding + $fine2->amountoutstanding, '80', "Total outstanding = 80" );
304 # Total : Outstanding : MaxFine
307 # Fix fine 1 - First item renewed
308 $fine->status('RETURNED')->store;
310 # Add fine 3 - First item second overdue
313 issue_id => $checkout1->issue_id,
314 itemnumber => $item1->itemnumber,
315 borrowernumber => $patron->borrowernumber,
317 due => $checkout1->date_due
321 $fines = Koha::Account::Lines->search(
322 { borrowernumber => $patron->borrowernumber },
323 { order_by => { '-asc' => 'accountlines_id' } }
325 is( $fines->count, 3, "Third fine added for overdue renewal" );
326 $fine = $fines->next;
327 is( $fine->amount+0, 80, "First fine amount unchanged" );
328 is( $fine->amountoutstanding+0, 50, "First fine amountoutstanding unchanged" );
329 $fine2 = $fines->next;
330 is( $fine2->amount+0, 30, "Second fine amount unchanged" );
331 is( $fine2->amountoutstanding+0, 30, "Second fine amountoutstanding unchanged" );
332 my $fine3 = $fines->next;
333 is( $fine3->amount+0, 20, "Third fine amount capped due to MaxFine" );
334 is( $fine3->amountoutstanding+0, 20, "Third fine amountoutstanding capped at '20' by MaxFine" );
335 is( $fine3->issue_id, $checkout1->issue_id, "Third fine is associated with the correct issue" );
336 is( $fine3->itemnumber, $checkout1->itemnumber, "Third fine is associated with the correct item" );
337 is( $fine->amount + $fine2->amount + $fine3->amount, '130', "Total fines = 130" );
338 is( $fine->amountoutstanding + $fine2->amountoutstanding + $fine3->amountoutstanding, '100', "Total outstanding = 100" );
339 # Total : Outstanding : MaxFine
342 # Payoff accruing fine and ensure next increment doesn't create a new one (bug #24146)
343 $fine3->amountoutstanding('0')->store;
344 is( $fine->amount + $fine2->amount + $fine3->amount, '130', "Total fines = 130" );
345 is( $fine->amountoutstanding + $fine2->amountoutstanding + $fine3->amountoutstanding, '80', "Total outstanding = 80" );
346 # Total : Outstanding : MaxFine
349 # Increase fine 3 - First item, second overdue increase
352 issue_id => $checkout1->issue_id,
353 itemnumber => $item1->itemnumber,
354 borrowernumber => $patron->borrowernumber,
356 due => $checkout1->date_due
360 $fines = Koha::Account::Lines->search(
361 { borrowernumber => $patron->borrowernumber },
362 { order_by => { '-asc' => 'accountlines_id' } }
364 is( $fines->count, 3, "Still three fines after third checkout update" );
365 $fine = $fines->next;
366 is( $fine->amount+0, 80, "First fine amount unchanged" );
367 is( $fine->amountoutstanding+0, 50, "First fine amountoutstanding unchanged" );
368 $fine2 = $fines->next;
369 is( $fine2->amount+0, 30, "Second fine amount unchanged" );
370 is( $fine2->amountoutstanding+0, 30, "Second fine amountoutstanding unchanged" );
371 $fine3 = $fines->next;
372 is( $fine3->amount+0, 40, "Third fine amount capped due to MaxFine" );
373 is( $fine3->amountoutstanding+0, 20, "Third fine amountoutstanding increased ..." );
374 is( $fine3->issue_id, $checkout1->issue_id, "Third fine is associated with the correct issue" );
375 is( $fine3->itemnumber, $checkout1->itemnumber, "Third fine is associated with the correct item" );
376 is( $fine->amount + $fine2->amount + $fine3->amount, '150', "Total fines = 150" );
377 is( $fine->amountoutstanding + $fine2->amountoutstanding + $fine3->amountoutstanding, '100', "Total outstanding = 100" );
378 # Total : Outstanding : MaxFine
381 # FIXME: Add test to check whether sundry/manual charges are included within MaxFine.
382 # FIXME: Add test to ensure other charges are not included within MaxFine.
385 t::lib::Mocks::mock_preference( 'MaxFine', '0' );
388 issue_id => $checkout1->issue_id,
389 itemnumber => $item1->itemnumber,
390 borrowernumber => $patron->borrowernumber,
392 due => $checkout1->date_due
396 $fines = Koha::Account::Lines->search(
397 { borrowernumber => $patron->borrowernumber },
398 { order_by => { '-asc' => 'accountlines_id' } }
400 is( $fines->count, 3, "Still only three fines after MaxFine cap removed" );
401 $fine = $fines->next;
402 is( $fine->amount+0, 80, "First fine amount unchanged" );
403 $fine2 = $fines->next;
404 is( $fine2->amount+0, 30, "Second fine amount unchanged" );
405 $fine3 = $fines->next;
406 is( $fine3->amount+0, 50, "Third fine increased now MaxFine cap is disabled" );
407 is( $fine3->amountoutstanding+0, 30, "Third fine increased now MaxFine cap is disabled" );
409 # If somehow the fine should be reduced, we changed rules or checkout date or something
412 issue_id => $checkout1->issue_id,
413 itemnumber => $item1->itemnumber,
414 borrowernumber => $patron->borrowernumber,
416 due => $checkout1->date_due
420 $fines = Koha::Account::Lines->search(
421 { borrowernumber => $patron->borrowernumber },
422 { order_by => { '-asc' => 'accountlines_id' } }
424 is( $fines->count, 3, "Still only three fines after MaxFine cap removed and third fine altered" );
425 $fine = $fines->next;
426 is( $fine->amount+0, 80, "First fine amount unchanged" );
427 $fine2 = $fines->next;
428 is( $fine2->amount+0, 30, "Second fine amount unchanged" );
429 $fine3 = $fines->next;
430 is( $fine3->amount+0, 30, "Third fine reduced" );
431 is( $fine3->amountoutstanding+0, 10, "Third fine amount outstanding is reduced" );
433 # Ensure calculations work correctly for floats (bug #25127)
434 # 7.2 (maxfine) - 7.2 (total_amount_other) != 8.88178419700125e-16 (😢)
435 t::lib::Mocks::mock_preference( 'MaxFine', '7.2' );
436 my $patron_1 = $builder->build_object( { class => 'Koha::Patrons' } );
437 my $item_1 = $builder->build_sample_item();
438 my $item_2 = $builder->build_sample_item();
439 my $checkout_1 = $builder->build_object(
441 class => 'Koha::Checkouts',
443 itemnumber => $item_1->itemnumber,
444 borrowernumber => $patron_1->id
448 my $checkout_2 = $builder->build_object(
450 class => 'Koha::Checkouts',
452 itemnumber => $item_2->itemnumber,
453 borrowernumber => $patron->id
457 my $account = $patron_1->account;
462 issue_id => $checkout_1->issue_id,
470 issue_id => $checkout_1->issue_id,
478 issue_id => $checkout_1->issue_id,
486 issue_id => $checkout_1->issue_id,
492 issue_id => $checkout_2->issue_id,
493 itemnumber => $item_2->itemnumber,
494 borrowernumber => $patron_1->borrowernumber,
496 due => $checkout_2->date_due
499 $fines = Koha::Account::Lines->search(
500 { borrowernumber => $patron_1->borrowernumber },
501 { order_by => { '-asc' => 'accountlines_id' } }
503 is( $fines->count, 4, "New amount should be 0 so no fine added" );
504 ok( C4::Circulation::AddReturn( $item_1->barcode, $item_1->homebranch, 1), "Returning the item and forgiving fines succeeds");
506 $schema->storage->txn_rollback;