Bug 22008: Fixes to make unit tests pass
[koha.git] / t / db_dependent / Koha / Account / Lines.t
1 #!/usr/bin/perl
2
3 # Copyright 2018 Koha Development team
4 #
5 # This file is part of Koha
6 #
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.
11 #
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.
16 #
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>
19
20 use Modern::Perl;
21
22 use Test::More tests => 7;
23 use Test::Exception;
24
25 use C4::Circulation qw/AddIssue AddReturn/;
26 use Koha::Account;
27 use Koha::Account::Lines;
28 use Koha::Account::Offsets;
29 use Koha::Items;
30
31 use t::lib::Mocks;
32 use t::lib::TestBuilder;
33
34 my $schema = Koha::Database->new->schema;
35 my $builder = t::lib::TestBuilder->new;
36
37 subtest 'item() tests' => sub {
38
39     plan tests => 3;
40
41     $schema->storage->txn_begin;
42
43     my $library = $builder->build( { source => 'Branch' } );
44     my $biblioitem = $builder->build( { source => 'Biblioitem' } );
45     my $patron = $builder->build( { source => 'Borrower' } );
46     my $item = Koha::Item->new(
47     {
48         biblionumber     => $biblioitem->{biblionumber},
49         biblioitemnumber => $biblioitem->{biblioitemnumber},
50         homebranch       => $library->{branchcode},
51         holdingbranch    => $library->{branchcode},
52         barcode          => 'some_barcode_12',
53         itype            => 'BK',
54     })->store;
55
56     my $line = Koha::Account::Line->new(
57     {
58         borrowernumber => $patron->{borrowernumber},
59         itemnumber     => $item->itemnumber,
60         accounttype    => "F",
61         amount         => 10,
62     })->store;
63
64     my $account_line_item = $line->item;
65     is( ref( $account_line_item ), 'Koha::Item', 'Koha::Account::Line->item should return a Koha::Item' );
66     is( $line->itemnumber, $account_line_item->itemnumber, 'Koha::Account::Line->item should return the correct item' );
67
68     $line->itemnumber(undef)->store;
69     is( $line->item, undef, 'Koha::Account::Line->item should return undef if no item linked' );
70
71     $schema->storage->txn_rollback;
72 };
73
74 subtest 'total_outstanding() tests' => sub {
75
76     plan tests => 5;
77
78     $schema->storage->txn_begin;
79
80     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
81
82     my $lines = Koha::Account::Lines->search({ borrowernumber => $patron->id });
83     is( $lines->total_outstanding, 0, 'total_outstanding returns 0 if no lines (undef case)' );
84
85     my $debit_1 = Koha::Account::Line->new(
86         {   borrowernumber    => $patron->id,
87             accounttype       => "F",
88             amount            => 10,
89             amountoutstanding => 10
90         }
91     )->store;
92
93     my $debit_2 = Koha::Account::Line->new(
94         {   borrowernumber    => $patron->id,
95             accounttype       => "F",
96             amount            => 10,
97             amountoutstanding => 10
98         }
99     )->store;
100
101     $lines = Koha::Account::Lines->search({ borrowernumber => $patron->id });
102     is( $lines->total_outstanding, 20, 'total_outstanding sums correctly' );
103
104     my $credit_1 = Koha::Account::Line->new(
105         {   borrowernumber    => $patron->id,
106             accounttype       => "F",
107             amount            => -10,
108             amountoutstanding => -10
109         }
110     )->store;
111
112     $lines = Koha::Account::Lines->search({ borrowernumber => $patron->id });
113     is( $lines->total_outstanding, 10, 'total_outstanding sums correctly' );
114
115     my $credit_2 = Koha::Account::Line->new(
116         {   borrowernumber    => $patron->id,
117             accounttype       => "F",
118             amount            => -10,
119             amountoutstanding => -10
120         }
121     )->store;
122
123     $lines = Koha::Account::Lines->search({ borrowernumber => $patron->id });
124     is( $lines->total_outstanding, 0, 'total_outstanding sums correctly' );
125
126     my $credit_3 = Koha::Account::Line->new(
127         {   borrowernumber    => $patron->id,
128             accounttype       => "F",
129             amount            => -100,
130             amountoutstanding => -100
131         }
132     )->store;
133
134     $lines = Koha::Account::Lines->search({ borrowernumber => $patron->id });
135     is( $lines->total_outstanding, -100, 'total_outstanding sums correctly' );
136
137     $schema->storage->txn_rollback;
138 };
139
140 subtest 'is_credit() and is_debit() tests' => sub {
141
142     plan tests => 4;
143
144     $schema->storage->txn_begin;
145
146     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
147     my $account = $patron->account;
148
149     my $credit = $account->add_credit({ amount => 100, user_id => $patron->id });
150
151     ok( $credit->is_credit, 'is_credit detects credits' );
152     ok( !$credit->is_debit, 'is_debit detects credits' );
153
154     my $debit = Koha::Account::Line->new(
155     {
156         borrowernumber => $patron->id,
157         accounttype    => "F",
158         amount         => 10,
159     })->store;
160
161     ok( !$debit->is_credit, 'is_credit detects debits' );
162     ok( $debit->is_debit, 'is_debit detects debits');
163
164     $schema->storage->txn_rollback;
165 };
166
167 subtest 'apply() tests' => sub {
168
169     plan tests => 24;
170
171     $schema->storage->txn_begin;
172
173     my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
174     my $account = $patron->account;
175
176     my $credit = $account->add_credit( { amount => 100, user_id => $patron->id } );
177
178     my $debit_1 = Koha::Account::Line->new(
179         {   borrowernumber    => $patron->id,
180             accounttype       => "F",
181             amount            => 10,
182             amountoutstanding => 10
183         }
184     )->store;
185
186     my $debit_2 = Koha::Account::Line->new(
187         {   borrowernumber    => $patron->id,
188             accounttype       => "F",
189             amount            => 100,
190             amountoutstanding => 100
191         }
192     )->store;
193
194     $credit->discard_changes;
195     $debit_1->discard_changes;
196
197     my $debits = Koha::Account::Lines->search({ accountlines_id => $debit_1->id });
198     my $remaining_credit = $credit->apply( { debits => $debits, offset_type => 'Manual Credit' } );
199     is( $remaining_credit * 1, 90, 'Remaining credit is correctly calculated' );
200     $credit->discard_changes;
201     is( $credit->amountoutstanding * -1, $remaining_credit, 'Remaining credit correctly stored' );
202
203     # re-read debit info
204     $debit_1->discard_changes;
205     is( $debit_1->amountoutstanding * 1, 0, 'Debit has been cancelled' );
206
207     my $offsets = Koha::Account::Offsets->search( { credit_id => $credit->id, debit_id => $debit_1->id } );
208     is( $offsets->count, 1, 'Only one offset is generated' );
209     my $THE_offset = $offsets->next;
210     is( $THE_offset->amount * 1, -10, 'Amount was calculated correctly (less than the available credit)' );
211     is( $THE_offset->type, 'Manual Credit', 'Passed type stored correctly' );
212
213     $debits = Koha::Account::Lines->search({ accountlines_id => $debit_2->id });
214     $remaining_credit = $credit->apply( { debits => $debits } );
215     is( $remaining_credit, 0, 'No remaining credit left' );
216     $credit->discard_changes;
217     is( $credit->amountoutstanding * 1, 0, 'No outstanding credit' );
218     $debit_2->discard_changes;
219     is( $debit_2->amountoutstanding * 1, 10, 'Outstanding amount decremented correctly' );
220
221     $offsets = Koha::Account::Offsets->search( { credit_id => $credit->id, debit_id => $debit_2->id } );
222     is( $offsets->count, 1, 'Only one offset is generated' );
223     $THE_offset = $offsets->next;
224     is( $THE_offset->amount * 1, -90, 'Amount was calculated correctly (less than the available credit)' );
225     is( $THE_offset->type, 'Credit Applied', 'Defaults to \'Credit Applied\' offset type' );
226
227     $debits = Koha::Account::Lines->search({ accountlines_id => $debit_1->id });
228     throws_ok
229         { $credit->apply({ debits => $debits }); }
230         'Koha::Exceptions::Account::NoAvailableCredit',
231         '->apply() can only be used with outstanding credits';
232
233     $debits = Koha::Account::Lines->search({ accountlines_id => $credit->id });
234     throws_ok
235         { $debit_1->apply({ debits => $debits }); }
236         'Koha::Exceptions::Account::IsNotCredit',
237         '->apply() can only be used with credits';
238
239     $debits = Koha::Account::Lines->search({ accountlines_id => $credit->id });
240     my $credit_3 = $account->add_credit({ amount => 1 });
241     throws_ok
242         { $credit_3->apply({ debits => $debits }); }
243         'Koha::Exceptions::Account::IsNotDebit',
244         '->apply() can only be applied to credits';
245
246     my $credit_2 = $account->add_credit({ amount => 20 });
247     my $debit_3  = Koha::Account::Line->new(
248         {   borrowernumber    => $patron->id,
249             accounttype       => "F",
250             amount            => 100,
251             amountoutstanding => 100
252         }
253     )->store;
254
255     $debits = Koha::Account::Lines->search({ accountlines_id => { -in => [ $debit_1->id, $debit_2->id, $debit_3->id, $credit->id ] } });
256     throws_ok {
257         $credit_2->apply( { debits => $debits, offset_type => 'Manual Credit' } ); }
258         'Koha::Exceptions::Account::IsNotDebit',
259         '->apply() rolls back if any of the passed lines is not a debit';
260
261     is( $debit_1->discard_changes->amountoutstanding * 1,   0, 'No changes to already cancelled debit' );
262     is( $debit_2->discard_changes->amountoutstanding * 1,  10, 'Debit cancelled' );
263     is( $debit_3->discard_changes->amountoutstanding * 1, 100, 'Outstanding amount correctly calculated' );
264     is( $credit_2->discard_changes->amountoutstanding * -1, 20, 'No changes made' );
265
266     $debits = Koha::Account::Lines->search({ accountlines_id => { -in => [ $debit_1->id, $debit_2->id, $debit_3->id ] } });
267     $remaining_credit = $credit_2->apply( { debits => $debits, offset_type => 'Manual Credit' } );
268
269     is( $debit_1->discard_changes->amountoutstanding * 1,  0, 'No changes to already cancelled debit' );
270     is( $debit_2->discard_changes->amountoutstanding * 1,  0, 'Debit cancelled' );
271     is( $debit_3->discard_changes->amountoutstanding * 1, 90, 'Outstanding amount correctly calculated' );
272     is( $credit_2->discard_changes->amountoutstanding * 1, 0, 'No remaining credit' );
273
274     $schema->storage->txn_rollback;
275 };
276
277 subtest 'Keep account info when related patron, staff or item is deleted' => sub {
278
279     plan tests => 3;
280
281     $schema->storage->txn_begin;
282
283     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
284     my $staff = $builder->build_object( { class => 'Koha::Patrons' } );
285     my $item = $builder->build_object({ class => 'Koha::Items' });
286     my $issue = $builder->build_object(
287         {
288             class => 'Koha::Checkouts',
289             value => { itemnumber => $item->itemnumber }
290         }
291     );
292     my $line = Koha::Account::Line->new(
293     {
294         borrowernumber => $patron->borrowernumber,
295         manager_id     => $staff->borrowernumber,
296         itemnumber     => $item->itemnumber,
297         accounttype    => "F",
298         amount         => 10,
299     })->store;
300
301     $issue->delete;
302     $item->delete;
303     $line = $line->get_from_storage;
304     is( $line->itemnumber, undef, "The account line should not be deleted when the related item is delete");
305
306     $staff->delete;
307     $line = $line->get_from_storage;
308     is( $line->manager_id, undef, "The account line should not be deleted when the related staff is delete");
309
310     $patron->delete;
311     $line = $line->get_from_storage;
312     is( $line->borrowernumber, undef, "The account line should not be deleted when the related patron is delete");
313
314     $schema->storage->txn_rollback;
315 };
316
317 subtest 'adjust() tests' => sub {
318
319     plan tests => 33;
320
321     $schema->storage->txn_begin;
322
323     # count logs before any actions
324     my $action_logs = $schema->resultset('ActionLog')->search()->count;
325
326     # Disable logs
327     t::lib::Mocks::mock_preference( 'FinesLog', 0 );
328
329     my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
330     my $account = $patron->account;
331
332     my $debit_1 = Koha::Account::Line->new(
333         {   borrowernumber    => $patron->id,
334             accounttype       => "F",
335             amount            => 10,
336             amountoutstanding => 10
337         }
338     )->store;
339
340     my $debit_2 = Koha::Account::Line->new(
341         {   borrowernumber    => $patron->id,
342             accounttype       => "FU",
343             amount            => 100,
344             amountoutstanding => 100
345         }
346     )->store;
347
348     my $credit = $account->add_credit( { amount => 40, user_id => $patron->id } );
349
350     throws_ok { $debit_1->adjust( { amount => 50, type => 'bad' } ) }
351     qr/Update type not recognised/, 'Exception thrown for unrecognised type';
352
353     throws_ok { $debit_1->adjust( { amount => 50, type => 'fine_update' } ) }
354     qr/Update type not allowed on this accounttype/,
355       'Exception thrown for type conflict';
356
357     # Increment an unpaid fine
358     $debit_2->adjust( { amount => 150, type => 'fine_update' } )->discard_changes;
359
360     is( $debit_2->amount * 1, 150, 'Fine amount was updated in full' );
361     is( $debit_2->amountoutstanding * 1, 150, 'Fine amountoutstanding was update in full' );
362     isnt( $debit_2->date, undef, 'Date has been set' );
363     is( $debit_2->lastincrement * 1, 50, 'lastincrement is the to the right value' );
364
365     my $offsets = Koha::Account::Offsets->search( { debit_id => $debit_2->id } );
366     is( $offsets->count, 1, 'An offset is generated for the increment' );
367     my $THIS_offset = $offsets->next;
368     is( $THIS_offset->amount * 1, 50, 'Amount was calculated correctly (increment by 50)' );
369     is( $THIS_offset->type, 'fine_increase', 'Adjust type stored correctly' );
370
371     is( $schema->resultset('ActionLog')->count(), $action_logs + 0, 'No log was added' );
372
373     # Update fine to partially paid
374     my $debits = Koha::Account::Lines->search({ accountlines_id => $debit_2->id });
375     $credit->apply( { debits => $debits, offset_type => 'Manual Credit' } );
376
377     $debit_2->discard_changes;
378     is( $debit_2->amount * 1, 150, 'Fine amount unaffected by partial payment' );
379     is( $debit_2->amountoutstanding * 1, 110, 'Fine amountoutstanding updated by partial payment' );
380
381     # Enable logs
382     t::lib::Mocks::mock_preference( 'FinesLog', 1 );
383
384     # Increment the partially paid fine
385     $debit_2->adjust( { amount => 160, type => 'fine_update' } )->discard_changes;
386
387     is( $debit_2->amount * 1, 160, 'Fine amount was updated in full' );
388     is( $debit_2->amountoutstanding * 1, 120, 'Fine amountoutstanding was updated by difference' );
389     is( $debit_2->lastincrement * 1, 10, 'lastincrement is the to the right value' );
390
391     $offsets = Koha::Account::Offsets->search( { debit_id => $debit_2->id } );
392     is( $offsets->count, 3, 'An offset is generated for the increment' );
393     $THIS_offset = $offsets->last;
394     is( $THIS_offset->amount * 1, 10, 'Amount was calculated correctly (increment by 10)' );
395     is( $THIS_offset->type, 'fine_increase', 'Adjust type stored correctly' );
396
397     is( $schema->resultset('ActionLog')->count(), $action_logs + 1, 'Log was added' );
398
399     # Decrement the partially paid fine, less than what was paid
400     $debit_2->adjust( { amount => 50, type => 'fine_update' } )->discard_changes;
401
402     is( $debit_2->amount * 1, 50, 'Fine amount was updated in full' );
403     is( $debit_2->amountoutstanding * 1, 10, 'Fine amountoutstanding was updated by difference' );
404     is( $debit_2->lastincrement * 1, -110, 'lastincrement is the to the right value' );
405
406     $offsets = Koha::Account::Offsets->search( { debit_id => $debit_2->id } );
407     is( $offsets->count, 4, 'An offset is generated for the decrement' );
408     $THIS_offset = $offsets->last;
409     is( $THIS_offset->amount * 1, -110, 'Amount was calculated correctly (decrement by 110)' );
410     is( $THIS_offset->type, 'fine_decrease', 'Adjust type stored correctly' );
411
412     # Decrement the partially paid fine, more than what was paid
413     $debit_2->adjust( { amount => 30, type => 'fine_update' } )->discard_changes;
414     is( $debit_2->amount * 1, 30, 'Fine amount was updated in full' );
415     is( $debit_2->amountoutstanding * 1, 0, 'Fine amountoutstanding was zeroed (payment was 40)' );
416     is( $debit_2->lastincrement * 1, -20, 'lastincrement is the to the right value' );
417
418     $offsets = Koha::Account::Offsets->search( { debit_id => $debit_2->id } );
419     is( $offsets->count, 5, 'An offset is generated for the decrement' );
420     $THIS_offset = $offsets->last;
421     is( $THIS_offset->amount * 1, -20, 'Amount was calculated correctly (decrement by 20)' );
422     is( $THIS_offset->type, 'fine_decrease', 'Adjust type stored correctly' );
423
424     my $overpayment_refund = $account->lines->last;
425     is( $overpayment_refund->amount * 1, -10, 'A new credit has been added' );
426     is( $overpayment_refund->description, 'Overpayment refund', 'Credit generated with the expected description' );
427
428     $schema->storage->txn_rollback;
429 };
430
431 subtest 'checkout() tests' => sub {
432     plan tests => 6;
433
434     $schema->storage->txn_begin;
435
436     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
437     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
438     my $item = $builder->build_sample_item;
439     my $account = $patron->account;
440
441     t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode });
442     my $checkout = AddIssue( $patron->unblessed, $item->barcode );
443
444     my $line = $account->add_debit({
445         amount   => 10,
446         item_id  => $item->itemnumber,
447         issue_id => $checkout->issue_id,
448         type     => 'fine',
449     });
450
451     my $line_checkout = $line->checkout;
452     is( ref($line_checkout), 'Koha::Checkout', 'Result type is correct' );
453     is( $line_checkout->issue_id, $checkout->issue_id, 'Koha::Account::Line->checkout should return the correct checkout');
454
455     my ( $returned, undef, $old_checkout) = C4::Circulation::AddReturn( $item->barcode, $library->branchcode );
456     is( $returned, 1, 'The item should have been returned' );
457
458     $line = $line->get_from_storage;
459     my $old_line_checkout = $line->checkout;
460     is( ref($old_line_checkout), 'Koha::Old::Checkout', 'Result type is correct' );
461     is( $old_line_checkout->issue_id, $old_checkout->issue_id, 'Koha::Account::Line->checkout should return the correct old_checkout' );
462
463     $line->issue_id(undef)->store;
464     is( $line->checkout, undef, 'Koha::Account::Line->checkout should return undef if no checkout linked' );
465
466     $schema->storage->txn_rollback;
467 };
468
469 1;