Bug 24146: Illustrate increasing and decreasing fine
[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 => 59;
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 }
148         }
149     );
150     my $checkout2 = $builder->build_object(
151         {
152             class => 'Koha::Checkouts',
153             value => { itemnumber => $item2->itemnumber }
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, '50.000000', "Fine amount correctly set to 50" );
190     is( $fine->amountoutstanding, '50.000000', "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, '80.000000', "Fine amount correctly updated to 80" );
212     is( $fine->amountoutstanding, '80.000000', "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, '80.000000', "First fine amount unchanged" );
234     is( $fine->amountoutstanding, '80.000000', "First fine amountoutstanding unchanged" );
235     my $fine2 = $fines->next;
236     is( $fine2->amount, '20.000000', "Second fine capped at '20' by MaxFine" );
237     is( $fine2->amountoutstanding, '20.000000', "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     # Partial pay fine 1
246     $fine->amountoutstanding('50.000000')->store;
247     # Total : Outstanding : MaxFine
248     #  100  :     50      :   100
249
250     # Increase fine 2 - Second item overdue
251     UpdateFine(
252         {
253             issue_id       => $checkout2->issue_id,
254             itemnumber     => $item2->itemnumber,
255             borrowernumber => $patron->borrowernumber,
256             amount         => '30',
257             due            => $checkout2->date_due
258         }
259     );
260
261     $fines = Koha::Account::Lines->search(
262         { borrowernumber => $patron->borrowernumber },
263         { order_by       => { '-asc' => 'accountlines_id' } }
264     );
265     is( $fines->count,        2,    "Still two fines after second checkout update" );
266     $fine = $fines->next;
267     is( $fine->amount, '80.000000', "First fine amount unchanged" );
268     is( $fine->amountoutstanding, '50.000000', "First fine amountoutstanding unchanged" );
269     $fine2 = $fines->next;
270     is( $fine2->amount, '30.000000', "Second fine increased after partial payment of first" );
271     is( $fine2->amountoutstanding, '30.000000', "Second fine amountoutstanding increased after partial payment of first" );
272     is( $fine->amount + $fine2->amount, '110', "Total fines = 100" );
273     is( $fine->amountoutstanding + $fine2->amountoutstanding, '80', "Total outstanding = 80" );
274     # Total : Outstanding : MaxFine
275     #  110  :     80      :   100
276
277     # Fix fine 1 - First item renewed
278     $fine->status('RETURNED')->store;
279
280     # Add fine 3 - First item second overdue
281     UpdateFine(
282         {
283             issue_id       => $checkout1->issue_id,
284             itemnumber     => $item1->itemnumber,
285             borrowernumber => $patron->borrowernumber,
286             amount         => '30',
287             due            => $checkout1->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,        3,    "Third fine added for overdue renewal" );
296     $fine = $fines->next;
297     is( $fine->amount, '80.000000', "First fine amount unchanged" );
298     is( $fine->amountoutstanding, '50.000000', "First fine amountoutstanding unchanged" );
299     $fine2 = $fines->next;
300     is( $fine2->amount, '30.000000', "Second fine amount unchanged" );
301     is( $fine2->amountoutstanding, '30.000000', "Second fine amountoutstanding unchanged" );
302     my $fine3 = $fines->next;
303     is( $fine3->amount, '20.000000', "Third fine amount capped due to MaxFine" );
304     is( $fine3->amountoutstanding, '20.000000', "Third fine amountoutstanding capped at '20' by MaxFine" );
305     is( $fine3->issue_id, $checkout1->issue_id, "Third fine is associated with the correct issue" );
306     is( $fine3->itemnumber, $checkout1->itemnumber, "Third fine is associated with the correct item" );
307     is( $fine->amount + $fine2->amount + $fine3->amount, '130', "Total fines = 130" );
308     is( $fine->amountoutstanding + $fine2->amountoutstanding + $fine3->amountoutstanding, '100', "Total outstanding = 100" );
309     # Total : Outstanding : MaxFine
310     #  130  :     100     :   100
311
312     # Payoff accruing fine and ensure next increment doesn't create a new one (bug #24146)
313     $fine3->amountoutstanding('0')->store;
314     is( $fine->amount + $fine2->amount + $fine3->amount, '130', "Total fines = 130" );
315     is( $fine->amountoutstanding + $fine2->amountoutstanding + $fine3->amountoutstanding, '80', "Total outstanding = 80" );
316     # Total : Outstanding : MaxFine
317     #  130  :      80     :   100
318
319     # Increase fine 3 - First item, second overdue increase
320     UpdateFine(
321         {
322             issue_id       => $checkout1->issue_id,
323             itemnumber     => $item1->itemnumber,
324             borrowernumber => $patron->borrowernumber,
325             amount         => '50',
326             due            => $checkout1->date_due
327         }
328     );
329
330     $fines = Koha::Account::Lines->search(
331         { borrowernumber => $patron->borrowernumber },
332         { order_by       => { '-asc' => 'accountlines_id' } }
333     );
334     is( $fines->count,        3,    "Still three fines after third checkout update" );
335     $fine = $fines->next;
336     is( $fine->amount, '80.000000', "First fine amount unchanged" );
337     is( $fine->amountoutstanding, '50.000000', "First fine amountoutstanding unchanged" );
338     $fine2 = $fines->next;
339     is( $fine2->amount, '30.000000', "Second fine amount unchanged" );
340     is( $fine2->amountoutstanding, '30.000000', "Second fine amountoutstanding unchanged" );
341     $fine3 = $fines->next;
342     is( $fine3->amount, '40.000000', "Third fine amount capped due to MaxFine" );
343     is( $fine3->amountoutstanding, '20.000000', "Third fine amountoutstanding increased ..." );
344     is( $fine3->issue_id, $checkout1->issue_id, "Third fine is associated with the correct issue" );
345     is( $fine3->itemnumber, $checkout1->itemnumber, "Third fine is associated with the correct item" );
346     is( $fine->amount + $fine2->amount + $fine3->amount, '150', "Total fines = 150" );
347     is( $fine->amountoutstanding + $fine2->amountoutstanding + $fine3->amountoutstanding, '100', "Total outstanding = 100" );
348     # Total : Outstanding : MaxFine
349     #  150  :      100     :   100
350
351     # FIXME: Add test to check whether sundry/manual charges are included within MaxFine.
352     # FIXME: Add test to ensure other charges are not included within MaxFine.
353
354     # Disable MaxFine
355     t::lib::Mocks::mock_preference( 'MaxFine', '0' );
356     UpdateFine(
357         {
358             issue_id       => $checkout1->issue_id,
359             itemnumber     => $item1->itemnumber,
360             borrowernumber => $patron->borrowernumber,
361             amount         => '50',
362             due            => $checkout1->date_due
363         }
364     );
365
366     $fines = Koha::Account::Lines->search(
367         { borrowernumber => $patron->borrowernumber },
368         { order_by       => { '-asc' => 'accountlines_id' } }
369     );
370     is( $fines->count,        3,    "Still only three fines after MaxFine cap removed" );
371     $fine = $fines->next;
372     is( $fine->amount, '80.000000', "First fine amount unchanged" );
373     $fine2 = $fines->next;
374     is( $fine2->amount, '30.000000', "Second fine amount unchanged" );
375     $fine3 = $fines->next;
376     is( $fine3->amount, '50.000000', "Third fine increased now MaxFine cap is disabled" );
377     is( $fine3->amountoutstanding, '30.000000', "Third fine increased now MaxFine cap is disabled" );
378
379     # If somehow the fine should be reduced, we changed rules or checkout date or something
380     UpdateFine(
381         {
382             issue_id       => $checkout1->issue_id,
383             itemnumber     => $item1->itemnumber,
384             borrowernumber => $patron->borrowernumber,
385             amount         => '30',
386             due            => $checkout1->date_due
387         }
388     );
389
390     $fines = Koha::Account::Lines->search(
391         { borrowernumber => $patron->borrowernumber },
392         { order_by       => { '-asc' => 'accountlines_id' } }
393     );
394     is( $fines->count,        3,    "Still only three fines after MaxFine cap removed and third fine altered" );
395     $fine = $fines->next;
396     is( $fine->amount, '80.000000', "First fine amount unchanged" );
397     $fine2 = $fines->next;
398     is( $fine2->amount, '30.000000', "Second fine amount unchanged" );
399     $fine3 = $fines->next;
400     is( $fine3->amount, '30.000000', "Third fine reduced" );
401     is( $fine3->amountoutstanding, '10.000000', "Third fine amount outstanding is reduced" );
402
403
404
405     $schema->storage->txn_rollback;
406 };