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