Bug 26384: Fix executable flags
[koha.git] / t / db_dependent / Overdues.t
1 #!/usr/bin/perl;
2
3 use Modern::Perl;
4 use Test::More tests => 17;
5
6 use C4::Context;
7 use Koha::Database;
8 use Koha::Libraries;
9
10 use t::lib::Mocks;
11 use t::lib::TestBuilder;
12
13 use_ok('C4::Overdues');
14 can_ok('C4::Overdues', 'GetOverdueMessageTransportTypes');
15 can_ok('C4::Overdues', 'GetBranchcodesWithOverdueRules');
16
17 my $schema = Koha::Database->new->schema;
18 my $builder = t::lib::TestBuilder->new;
19
20 $schema->storage->txn_begin;
21 my $dbh = C4::Context->dbh;
22
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|);
28
29 $dbh->do(q|
30     INSERT INTO message_transport_types( message_transport_type ) VALUES ('email'), ('phone'), ('print'), ('sms')
31 |);
32
33 $dbh->do(q|
34     INSERT INTO overduerules ( overduerules_id, branchcode, categorycode ) VALUES
35     (1, 'CPL', 'PT'),
36     (2, 'CPL', 'YA'),
37     (3, '', 'PT'),
38     (4, '', 'YA')
39 |);
40
41 $dbh->do(q|INSERT INTO overduerules_transport_types (overduerules_id, letternumber, message_transport_type) VALUES
42     (1, 1, 'email'),
43     (1, 2, 'sms'),
44     (1, 3, 'email'),
45     (2, 3, 'print'),
46     (3, 1, 'email'),
47     (3, 2, 'email'),
48     (3, 2, 'sms'),
49     (3, 3, 'sms'),
50     (3, 3, 'email'),
51     (3, 3, 'print'),
52     (4, 2, 'sms')
53 |);
54
55 my $mtts;
56
57 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL', 'PT');
58 is( $mtts, undef, 'GetOverdueMessageTransportTypes: returns undef if no letternumber given' );
59
60 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL', undef, 1);
61 is( $mtts, undef, 'GetOverdueMessageTransportTypes: returns undef if no categorycode given' );
62
63 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL');
64 is( $mtts, undef, 'GetOverdueMessageTransportTypes: returns undef if no letternumber and categorycode given' );
65
66 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL', 'PT', 1);
67 is_deeply( $mtts, ['email'], 'GetOverdueMessageTransportTypes: first overdue is by email for PT (CPL)' );
68
69 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL', 'PT', 2);
70 is_deeply( $mtts, ['sms'], 'GetOverdueMessageTransportTypes: second overdue is by sms for PT (CPL)' );
71
72 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('CPL', 'PT', 3);
73 is_deeply( $mtts, ['email'], 'GetOverdueMessageTransportTypes: third overdue is by email for PT (CPL)' );
74
75 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('', 'PT', 1);
76 is_deeply( $mtts, ['email'], 'GetOverdueMessageTransportTypes: first overdue is by email for PT (default)' );
77
78 $mtts = C4::Overdues::GetOverdueMessageTransportTypes('', 'PT', 2);
79 is_deeply( $mtts, ['email', 'sms'], 'GetOverdueMessageTransportTypes: second overdue is by email and sms for PT (default)' );
80
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.' );
83
84 # Test GetBranchcodesWithOverdueRules
85 $dbh->do(q|DELETE FROM overduerules|);
86 $dbh->do(q|
87     INSERT INTO overduerules
88         ( branchcode,categorycode, delay1,letter1,debarred1, delay2,letter2,debarred2, delay3,letter3,debarred3 )
89         VALUES
90         ( '', '', 1, 'LETTER_CODE1', 1, 5, 'LETTER_CODE2', 1, 10, 'LETTER_CODE3', 1 )
91 |);
92
93 my @branchcodes = map { $_->branchcode } Koha::Libraries->search;
94
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' );
97
98 $dbh->do(q|
99     INSERT INTO overduerules
100         ( branchcode,categorycode, delay1,letter1,debarred1, delay2,letter2,debarred2, delay3,letter3,debarred3 )
101         VALUES
102         ( 'CPL', '', 1, 'LETTER_CODE1', 1, 5, 'LETTER_CODE2', 1, 10, 'LETTER_CODE3', 1 )
103 |);
104
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' );
107
108 $dbh->do(q|DELETE FROM overduerules|);
109 $dbh->do(q|
110     INSERT INTO overduerules
111         ( branchcode,categorycode, delay1,letter1,debarred1, delay2,letter2,debarred2, delay3,letter3,debarred3 )
112         VALUES
113         ( 'CPL', '', 1, 'LETTER_CODE1', 1, 5, 'LETTER_CODE2', 1, 10, 'LETTER_CODE3', 1 )
114 |);
115
116 @overdue_branches = C4::Overdues::GetBranchcodesWithOverdueRules();
117 is_deeply( \@overdue_branches, ['CPL'] , 'If only a specific rule exist, only 1 branch should be returned' );
118
119 $dbh->do(q|DELETE FROM overduerules|);
120 $dbh->do(q|
121     INSERT INTO overduerules
122         ( branchcode,categorycode, delay1,letter1,debarred1, delay2,letter2,debarred2, delay3,letter3,debarred3 )
123         VALUES
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 )
126 |);
127
128 @overdue_branches = C4::Overdues::GetBranchcodesWithOverdueRules();
129 is_deeply( \@overdue_branches, ['CPL', 'MPL'] , 'If only 2 specific rules exist, 2 branches should be returned' );
130
131 $schema->storage->txn_rollback;
132
133 subtest 'UpdateFine tests' => sub {
134
135     plan tests => 70;
136
137     $schema->storage->txn_begin;
138
139     t::lib::Mocks::mock_preference( 'MaxFine', '100' );
140
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(
145         {
146             class => 'Koha::Checkouts',
147             value => { itemnumber => $item1->itemnumber, borrowernumber => $patron->id }
148         }
149     );
150     my $checkout2 = $builder->build_object(
151         {
152             class => 'Koha::Checkouts',
153             value => { itemnumber => $item2->itemnumber, borrowernumber => $patron->id }
154         }
155     );
156
157     # Try to add 0 amount fine
158     UpdateFine(
159         {
160             issue_id       => $checkout1->issue_id,
161             itemnumber     => $item1->itemnumber,
162             borrowernumber => $patron->borrowernumber,
163             amount         => '0',
164             due            => $checkout1->date_due
165         }
166     );
167
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
172     #   0   :      0      :   100
173
174     # Add fine 1 - First Item Overdue
175     UpdateFine(
176         {
177             issue_id       => $checkout1->issue_id,
178             itemnumber     => $item1->itemnumber,
179             borrowernumber => $patron->borrowernumber,
180             amount         => '50',
181             due            => $checkout1->date_due
182         }
183     );
184
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
194     #  50   :     50      :   100
195
196     # Increase fine 1 - First Item Overdue
197     UpdateFine(
198         {
199             issue_id       => $checkout1->issue_id,
200             itemnumber     => $item1->itemnumber,
201             borrowernumber => $patron->borrowernumber,
202             amount         => '80',
203             due            => $checkout1->date_due
204         }
205     );
206
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
214     #  80   :     80      :   100
215
216     # Add fine 2 - Second Item Overdue
217     UpdateFine(
218         {
219             issue_id       => $checkout2->issue_id,
220             itemnumber     => $item2->itemnumber,
221             borrowernumber => $patron->borrowernumber,
222             amount         => '30',
223             due            => $checkout2->date_due
224         }
225     );
226
227     $fines = Koha::Account::Lines->search(
228         { borrowernumber => $patron->borrowernumber },
229         { order_by       => { '-asc' => 'accountlines_id' } }
230     );
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
243     #  100  :     100     :   100
244
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
247     UpdateFine(
248         {
249             issue_id       => $checkout2->issue_id,
250             itemnumber     => $item2->itemnumber,
251             borrowernumber => $patron->borrowernumber,
252             amount         => '40',
253             due            => $checkout2->date_due
254         }
255     );
256
257     $fines = Koha::Account::Lines->search(
258         { borrowernumber => $patron->borrowernumber },
259         { order_by       => { '-asc' => 'accountlines_id' } }
260     );
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
273     #  100  :     100     :   100
274
275     # Partial pay fine 1
276     $fine->amountoutstanding(50)->store;
277     # Total : Outstanding : MaxFine
278     #  100  :     50      :   100
279
280     # Increase fine 2 - Second item overdue
281     UpdateFine(
282         {
283             issue_id       => $checkout2->issue_id,
284             itemnumber     => $item2->itemnumber,
285             borrowernumber => $patron->borrowernumber,
286             amount         => '30',
287             due            => $checkout2->date_due
288         }
289     );
290
291     $fines = Koha::Account::Lines->search(
292         { borrowernumber => $patron->borrowernumber },
293         { order_by       => { '-asc' => 'accountlines_id' } }
294     );
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
305     #  110  :     80      :   100
306
307     # Fix fine 1 - First item renewed
308     $fine->status('RETURNED')->store;
309
310     # Add fine 3 - First item second overdue
311     UpdateFine(
312         {
313             issue_id       => $checkout1->issue_id,
314             itemnumber     => $item1->itemnumber,
315             borrowernumber => $patron->borrowernumber,
316             amount         => '30',
317             due            => $checkout1->date_due
318         }
319     );
320
321     $fines = Koha::Account::Lines->search(
322         { borrowernumber => $patron->borrowernumber },
323         { order_by       => { '-asc' => 'accountlines_id' } }
324     );
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
340     #  130  :     100     :   100
341
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
347     #  130  :      80     :   100
348
349     # Increase fine 3 - First item, second overdue increase
350     UpdateFine(
351         {
352             issue_id       => $checkout1->issue_id,
353             itemnumber     => $item1->itemnumber,
354             borrowernumber => $patron->borrowernumber,
355             amount         => '50',
356             due            => $checkout1->date_due
357         }
358     );
359
360     $fines = Koha::Account::Lines->search(
361         { borrowernumber => $patron->borrowernumber },
362         { order_by       => { '-asc' => 'accountlines_id' } }
363     );
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
379     #  150  :      100     :   100
380
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.
383
384     # Disable MaxFine
385     t::lib::Mocks::mock_preference( 'MaxFine', '0' );
386     UpdateFine(
387         {
388             issue_id       => $checkout1->issue_id,
389             itemnumber     => $item1->itemnumber,
390             borrowernumber => $patron->borrowernumber,
391             amount         => '50',
392             due            => $checkout1->date_due
393         }
394     );
395
396     $fines = Koha::Account::Lines->search(
397         { borrowernumber => $patron->borrowernumber },
398         { order_by       => { '-asc' => 'accountlines_id' } }
399     );
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" );
408
409     # If somehow the fine should be reduced, we changed rules or checkout date or something
410     UpdateFine(
411         {
412             issue_id       => $checkout1->issue_id,
413             itemnumber     => $item1->itemnumber,
414             borrowernumber => $patron->borrowernumber,
415             amount         => '30',
416             due            => $checkout1->date_due
417         }
418     );
419
420     $fines = Koha::Account::Lines->search(
421         { borrowernumber => $patron->borrowernumber },
422         { order_by       => { '-asc' => 'accountlines_id' } }
423     );
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" );
432
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(
440         {
441             class => 'Koha::Checkouts',
442             value => {
443                 itemnumber     => $item_1->itemnumber,
444                 borrowernumber => $patron_1->id
445             }
446         }
447     );
448     my $checkout_2 = $builder->build_object(
449         {
450             class => 'Koha::Checkouts',
451             value => {
452                 itemnumber     => $item_2->itemnumber,
453                 borrowernumber => $patron->id
454             }
455         }
456     );
457     my $account = $patron_1->account;
458     $account->add_debit(
459         {
460             type      => 'OVERDUE',
461             amount    => '6.99',
462             issue_id  => $checkout_1->issue_id,
463             interface => 'TEST'
464         }
465     );
466     $account->add_debit(
467         {
468             type      => 'OVERDUE',
469             amount    => '.10',
470             issue_id  => $checkout_1->issue_id,
471             interface => 'TEST'
472         }
473     );
474     $account->add_debit(
475         {
476             type      => 'OVERDUE',
477             amount    => '.10',
478             issue_id  => $checkout_1->issue_id,
479             interface => 'TEST'
480         }
481     );
482     $account->add_debit(
483         {
484             type      => 'OVERDUE',
485             amount    => '.01',
486             issue_id  => $checkout_1->issue_id,
487             interface => 'TEST'
488         }
489     );
490     UpdateFine(
491         {
492             issue_id       => $checkout_2->issue_id,
493             itemnumber     => $item_2->itemnumber,
494             borrowernumber => $patron_1->borrowernumber,
495             amount         => '.1',
496             due            => $checkout_2->date_due
497         }
498     );
499     $fines = Koha::Account::Lines->search(
500         { borrowernumber => $patron_1->borrowernumber },
501         { order_by       => { '-asc' => 'accountlines_id' } }
502     );
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");
505
506     $schema->storage->txn_rollback;
507 };