Bug 27636: (QA follow-up) Fix tests and validate
[koha.git] / Koha / Account.pm
1 package Koha::Account;
2
3 # Copyright 2016 ByWater Solutions
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 Carp;
23 use Data::Dumper;
24 use List::MoreUtils qw( uniq );
25 use Try::Tiny;
26
27 use C4::Circulation qw( ReturnLostItem CanBookBeRenewed AddRenewal );
28 use C4::Letters;
29 use C4::Log qw( logaction );
30 use C4::Stats qw( UpdateStats );
31 use C4::Overdues qw(GetFine);
32
33 use Koha::Patrons;
34 use Koha::Account::Lines;
35 use Koha::Account::Offsets;
36 use Koha::Account::DebitTypes;
37 use Koha::DateUtils qw( dt_from_string );
38 use Koha::Exceptions;
39 use Koha::Exceptions::Account;
40
41 =head1 NAME
42
43 Koha::Accounts - Module for managing payments and fees for patrons
44
45 =cut
46
47 sub new {
48     my ( $class, $params ) = @_;
49
50     Carp::croak("No patron id passed in!") unless $params->{patron_id};
51
52     return bless( $params, $class );
53 }
54
55 =head2 pay
56
57 This method allows payments to be made against fees/fines
58
59 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
60     {
61         amount      => $amount,
62         note        => $note,
63         description => $description,
64         library_id  => $branchcode,
65         lines       => $lines, # Arrayref of Koha::Account::Line objects to pay
66         credit_type => $type,  # credit_type_code code
67         offset_type => $offset_type,    # offset type code
68         item_id     => $itemnumber,     # pass the itemnumber if this is a credit pertianing to a specific item (i.e LOST_FOUND)
69     }
70 );
71
72 =cut
73
74 sub pay {
75     my ( $self, $params ) = @_;
76
77     my $amount        = $params->{amount};
78     my $description   = $params->{description};
79     my $note          = $params->{note} || q{};
80     my $library_id    = $params->{library_id};
81     my $lines         = $params->{lines};
82     my $type          = $params->{type} || 'PAYMENT';
83     my $payment_type  = $params->{payment_type} || undef;
84     my $cash_register = $params->{cash_register};
85     my $item_id       = $params->{item_id};
86
87     my $userenv = C4::Context->userenv;
88
89
90     my $manager_id = $userenv ? $userenv->{number} : undef;
91     my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
92     my $payment = $self->payin_amount(
93         {
94             interface     => $interface,
95             type          => $type,
96             amount        => $amount,
97             payment_type  => $payment_type,
98             cash_register => $cash_register,
99             user_id       => $manager_id,
100             library_id    => $library_id,
101             item_id       => $item_id,
102             description   => $description,
103             note          => $note,
104             debits        => $lines
105         }
106     );
107
108     my $patron = Koha::Patrons->find( $self->{patron_id} );
109     my @account_offsets = $payment->debit_offsets;
110     if ( C4::Context->preference('UseEmailReceipts') ) {
111         if (
112             my $letter = C4::Letters::GetPreparedLetter(
113                 module                 => 'circulation',
114                 letter_code            => uc("ACCOUNT_$type"),
115                 message_transport_type => 'email',
116                 lang    => $patron->lang,
117                 tables => {
118                     borrowers       => $self->{patron_id},
119                     branches        => $library_id,
120                 },
121                 substitute => {
122                     credit => $payment,
123                     offsets => \@account_offsets,
124                 },
125               )
126           )
127         {
128             C4::Letters::EnqueueLetter(
129                 {
130                     letter                 => $letter,
131                     borrowernumber         => $self->{patron_id},
132                     message_transport_type => 'email',
133                 }
134             ) or warn "can't enqueue letter $letter";
135         }
136     }
137
138     my $renew_outcomes = [];
139     for my $message ( @{$payment->messages} ) {
140         push @{$renew_outcomes}, $message->payload;
141     }
142
143     return { payment_id => $payment->id, renew_result => $renew_outcomes };
144 }
145
146 =head3 add_credit
147
148 This method allows adding credits to a patron's account
149
150 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
151     {
152         amount       => $amount,
153         description  => $description,
154         note         => $note,
155         user_id      => $user_id,
156         interface    => $interface,
157         library_id   => $library_id,
158         payment_type => $payment_type,
159         type         => $credit_type,
160         item_id      => $item_id
161     }
162 );
163
164 $credit_type can be any of:
165   - 'CREDIT'
166   - 'PAYMENT'
167   - 'FORGIVEN'
168   - 'LOST_FOUND'
169   - 'OVERPAYMENT'
170   - 'PAYMENT'
171   - 'WRITEOFF'
172
173 =cut
174
175 sub add_credit {
176
177     my ( $self, $params ) = @_;
178
179     # check for mandatory params
180     my @mandatory = ( 'interface', 'amount' );
181     for my $param (@mandatory) {
182         unless ( defined( $params->{$param} ) ) {
183             Koha::Exceptions::MissingParameter->throw(
184                 error => "The $param parameter is mandatory" );
185         }
186     }
187
188     # amount should always be passed as a positive value
189     my $amount = $params->{amount} * -1;
190     unless ( $amount < 0 ) {
191         Koha::Exceptions::Account::AmountNotPositive->throw(
192             error => 'Debit amount passed is not positive' );
193     }
194
195     my $description   = $params->{description} // q{};
196     my $note          = $params->{note} // q{};
197     my $user_id       = $params->{user_id};
198     my $interface     = $params->{interface};
199     my $library_id    = $params->{library_id};
200     my $cash_register = $params->{cash_register};
201     my $payment_type  = $params->{payment_type};
202     my $credit_type   = $params->{type} || 'PAYMENT';
203     my $item_id       = $params->{item_id};
204
205     Koha::Exceptions::Account::RegisterRequired->throw()
206       if ( C4::Context->preference("UseCashRegisters")
207         && defined($payment_type)
208         && ( $payment_type eq 'CASH' )
209         && !defined($cash_register) );
210
211     my $line;
212     my $schema = Koha::Database->new->schema;
213     try {
214         $schema->txn_do(
215             sub {
216
217                 # Insert the account line
218                 $line = Koha::Account::Line->new(
219                     {
220                         borrowernumber    => $self->{patron_id},
221                         date              => \'NOW()',
222                         amount            => $amount,
223                         description       => $description,
224                         credit_type_code  => $credit_type,
225                         amountoutstanding => $amount,
226                         payment_type      => $payment_type,
227                         note              => $note,
228                         manager_id        => $user_id,
229                         interface         => $interface,
230                         branchcode        => $library_id,
231                         register_id       => $cash_register,
232                         itemnumber        => $item_id,
233                     }
234                 )->store();
235
236                 # Record the account offset
237                 my $account_offset = Koha::Account::Offset->new(
238                     {
239                         credit_id => $line->id,
240                         type   => $Koha::Account::offset_type->{$credit_type} // $Koha::Account::offset_type->{CREDIT},
241                         amount => $amount
242                     }
243                 )->store();
244
245                 C4::Stats::UpdateStats(
246                     {
247                         branch         => $library_id,
248                         type           => lc($credit_type),
249                         amount         => $amount,
250                         borrowernumber => $self->{patron_id},
251                     }
252                 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
253
254                 if ( C4::Context->preference("FinesLog") ) {
255                     logaction(
256                         "FINES", 'CREATE',
257                         $self->{patron_id},
258                         Dumper(
259                             {
260                                 action            => "create_$credit_type",
261                                 borrowernumber    => $self->{patron_id},
262                                 amount            => $amount,
263                                 description       => $description,
264                                 amountoutstanding => $amount,
265                                 credit_type_code  => $credit_type,
266                                 note              => $note,
267                                 itemnumber        => $item_id,
268                                 manager_id        => $user_id,
269                                 branchcode        => $library_id,
270                             }
271                         ),
272                         $interface
273                     );
274                 }
275             }
276         );
277     }
278     catch {
279         if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
280             if ( $_->broken_fk eq 'credit_type_code' ) {
281                 Koha::Exceptions::Account::UnrecognisedType->throw(
282                     error => 'Type of credit not recognised' );
283             }
284             else {
285                 $_->rethrow;
286             }
287         }
288     };
289
290     return $line;
291 }
292
293 =head3 payin_amount
294
295     my $credit = $account->payin_amount(
296         {
297             amount          => $amount,
298             type            => $credit_type,
299             payment_type    => $payment_type,
300             cash_register   => $register_id,
301             interface       => $interface,
302             library_id      => $branchcode,
303             user_id         => $staff_id,
304             debits          => $debit_lines,
305             description     => $description,
306             note            => $note
307         }
308     );
309
310 This method allows an amount to be paid into a patrons account and immediately applied against debts.
311
312 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
313
314 $credit_type can be any of:
315   - 'PAYMENT'
316   - 'WRITEOFF'
317   - 'FORGIVEN'
318
319 =cut
320
321 sub payin_amount {
322     my ( $self, $params ) = @_;
323
324     # check for mandatory params
325     my @mandatory = ( 'interface', 'amount', 'type' );
326     for my $param (@mandatory) {
327         unless ( defined( $params->{$param} ) ) {
328             Koha::Exceptions::MissingParameter->throw(
329                 error => "The $param parameter is mandatory" );
330         }
331     }
332
333     # Check for mandatory register
334     Koha::Exceptions::Account::RegisterRequired->throw()
335       if ( C4::Context->preference("UseCashRegisters")
336         && defined( $params->{payment_type} )
337         && ( $params->{payment_type} eq 'CASH' )
338         && !defined($params->{cash_register}) );
339
340     # amount should always be passed as a positive value
341     my $amount = $params->{amount};
342     unless ( $amount > 0 ) {
343         Koha::Exceptions::Account::AmountNotPositive->throw(
344             error => 'Payin amount passed is not positive' );
345     }
346
347     my $credit;
348     my $schema = Koha::Database->new->schema;
349     $schema->txn_do(
350         sub {
351
352             # Add payin credit
353             $credit = $self->add_credit($params);
354
355             # Offset debts passed first
356             if ( exists( $params->{debits} ) ) {
357                 $credit = $credit->apply(
358                     {
359                         debits      => $params->{debits},
360                         offset_type => $Koha::Account::offset_type->{$params->{type}}
361                     }
362                 );
363             }
364
365             # Offset against remaining balance if AutoReconcile
366             if ( C4::Context->preference("AccountAutoReconcile")
367                 && $credit->amountoutstanding != 0 )
368             {
369                 $credit = $credit->apply(
370                     {
371                         debits      => [ $self->outstanding_debits->as_list ],
372                         offset_type => $Koha::Account::offset_type->{$params->{type}}
373                     }
374                 );
375             }
376         }
377     );
378
379     return $credit;
380 }
381
382 =head3 add_debit
383
384 This method allows adding debits to a patron's account
385
386     my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
387         {
388             amount           => $amount,
389             description      => $description,
390             note             => $note,
391             user_id          => $user_id,
392             interface        => $interface,
393             library_id       => $library_id,
394             type             => $debit_type,
395             transaction_type => $transaction_type,
396             cash_register    => $register_id,
397             item_id          => $item_id,
398             issue_id         => $issue_id
399         }
400     );
401
402 $debit_type can be any of:
403   - ACCOUNT
404   - ACCOUNT_RENEW
405   - RESERVE_EXPIRED
406   - LOST
407   - sundry
408   - NEW_CARD
409   - OVERDUE
410   - PROCESSING
411   - RENT
412   - RENT_DAILY
413   - RENT_RENEW
414   - RENT_DAILY_RENEW
415   - RESERVE
416   - PAYOUT
417
418 =cut
419
420 sub add_debit {
421
422     my ( $self, $params ) = @_;
423
424     # check for mandatory params
425     my @mandatory = ( 'interface', 'type', 'amount' );
426     for my $param (@mandatory) {
427         unless ( defined( $params->{$param} ) ) {
428             Koha::Exceptions::MissingParameter->throw(
429                 error => "The $param parameter is mandatory" );
430         }
431     }
432
433     # check for cash register if using cash
434     Koha::Exceptions::Account::RegisterRequired->throw()
435       if ( C4::Context->preference("UseCashRegisters")
436         && defined( $params->{transaction_type} )
437         && ( $params->{transaction_type} eq 'CASH' )
438         && !defined( $params->{cash_register} ) );
439
440     # amount should always be a positive value
441     my $amount = $params->{amount};
442     unless ( $amount > 0 ) {
443         Koha::Exceptions::Account::AmountNotPositive->throw(
444             error => 'Debit amount passed is not positive' );
445     }
446
447     my $description      = $params->{description} // q{};
448     my $note             = $params->{note} // q{};
449     my $user_id          = $params->{user_id};
450     my $interface        = $params->{interface};
451     my $library_id       = $params->{library_id};
452     my $cash_register    = $params->{cash_register};
453     my $debit_type       = $params->{type};
454     my $transaction_type = $params->{transaction_type};
455     my $item_id          = $params->{item_id};
456     my $issue_id         = $params->{issue_id};
457     my $offset_type      = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
458
459     my $line;
460     my $schema = Koha::Database->new->schema;
461     try {
462         $schema->txn_do(
463             sub {
464
465                 # Insert the account line
466                 $line = Koha::Account::Line->new(
467                     {
468                         borrowernumber    => $self->{patron_id},
469                         date              => \'NOW()',
470                         amount            => $amount,
471                         description       => $description,
472                         debit_type_code   => $debit_type,
473                         amountoutstanding => $amount,
474                         payment_type      => $transaction_type,
475                         note              => $note,
476                         manager_id        => $user_id,
477                         interface         => $interface,
478                         itemnumber        => $item_id,
479                         issue_id          => $issue_id,
480                         branchcode        => $library_id,
481                         register_id       => $cash_register,
482                         (
483                             $debit_type eq 'OVERDUE'
484                             ? ( status => 'UNRETURNED' )
485                             : ()
486                         ),
487                     }
488                 )->store();
489
490                 # Record the account offset
491                 my $account_offset = Koha::Account::Offset->new(
492                     {
493                         debit_id => $line->id,
494                         type     => $offset_type,
495                         amount   => $amount
496                     }
497                 )->store();
498
499                 if ( C4::Context->preference("FinesLog") ) {
500                     logaction(
501                         "FINES", 'CREATE',
502                         $self->{patron_id},
503                         Dumper(
504                             {
505                                 action            => "create_$debit_type",
506                                 borrowernumber    => $self->{patron_id},
507                                 amount            => $amount,
508                                 description       => $description,
509                                 amountoutstanding => $amount,
510                                 debit_type_code   => $debit_type,
511                                 note              => $note,
512                                 itemnumber        => $item_id,
513                                 manager_id        => $user_id,
514                             }
515                         ),
516                         $interface
517                     );
518                 }
519             }
520         );
521     }
522     catch {
523         if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
524             if ( $_->broken_fk eq 'debit_type_code' ) {
525                 Koha::Exceptions::Account::UnrecognisedType->throw(
526                     error => 'Type of debit not recognised' );
527             }
528             else {
529                 $_->rethrow;
530             }
531         }
532     };
533
534     return $line;
535 }
536
537 =head3 payout_amount
538
539     my $debit = $account->payout_amount(
540         {
541             payout_type => $payout_type,
542             register_id => $register_id,
543             staff_id    => $staff_id,
544             interface   => 'intranet',
545             amount      => $amount,
546             credits     => $credit_lines
547         }
548     );
549
550 This method allows an amount to be paid out from a patrons account against outstanding credits.
551
552 $payout_type can be any of the defined payment_types:
553
554 =cut
555
556 sub payout_amount {
557     my ( $self, $params ) = @_;
558
559     # Check for mandatory parameters
560     my @mandatory =
561       ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
562     for my $param (@mandatory) {
563         unless ( defined( $params->{$param} ) ) {
564             Koha::Exceptions::MissingParameter->throw(
565                 error => "The $param parameter is mandatory" );
566         }
567     }
568
569     # Check for mandatory register
570     Koha::Exceptions::Account::RegisterRequired->throw()
571       if ( C4::Context->preference("UseCashRegisters")
572         && ( $params->{payout_type} eq 'CASH' )
573         && !defined($params->{cash_register}) );
574
575     # Amount should always be passed as a positive value
576     my $amount = $params->{amount};
577     unless ( $amount > 0 ) {
578         Koha::Exceptions::Account::AmountNotPositive->throw(
579             error => 'Payout amount passed is not positive' );
580     }
581
582     # Amount should always be less than or equal to outstanding credit
583     my $outstanding = 0;
584     my $outstanding_credits =
585       exists( $params->{credits} )
586       ? $params->{credits}
587       : $self->outstanding_credits->as_list;
588     for my $credit ( @{$outstanding_credits} ) {
589         $outstanding += $credit->amountoutstanding;
590     }
591     $outstanding = $outstanding * -1;
592     Koha::Exceptions::ParameterTooHigh->throw( error =>
593 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
594     ) unless ( $outstanding >= $amount );
595
596     my $payout;
597     my $schema = Koha::Database->new->schema;
598     $schema->txn_do(
599         sub {
600
601             # A 'payout' is a 'debit'
602             $payout = $self->add_debit(
603                 {
604                     amount            => $params->{amount},
605                     type              => 'PAYOUT',
606                     transaction_type  => $params->{payout_type},
607                     amountoutstanding => $params->{amount},
608                     manager_id        => $params->{staff_id},
609                     interface         => $params->{interface},
610                     branchcode        => $params->{branch},
611                     cash_register     => $params->{cash_register}
612                 }
613             );
614
615             # Offset against credits
616             for my $credit ( @{$outstanding_credits} ) {
617                 $credit->apply(
618                     { debits => [$payout], offset_type => 'PAYOUT' } );
619                 $payout->discard_changes;
620                 last if $payout->amountoutstanding == 0;
621             }
622
623             # Set payout as paid
624             $payout->status('PAID')->store;
625         }
626     );
627
628     return $payout;
629 }
630
631 =head3 balance
632
633 my $balance = $self->balance
634
635 Return the balance (sum of amountoutstanding columns)
636
637 =cut
638
639 sub balance {
640     my ($self) = @_;
641     return $self->lines->total_outstanding;
642 }
643
644 =head3 outstanding_debits
645
646 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
647
648 It returns the debit lines with outstanding amounts for the patron.
649
650 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
651 return a list of Koha::Account::Line objects.
652
653 =cut
654
655 sub outstanding_debits {
656     my ($self) = @_;
657
658     return $self->lines->search(
659         {
660             amount            => { '>' => 0 },
661             amountoutstanding => { '>' => 0 }
662         }
663     );
664 }
665
666 =head3 outstanding_credits
667
668 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
669
670 It returns the credit lines with outstanding amounts for the patron.
671
672 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
673 return a list of Koha::Account::Line objects.
674
675 =cut
676
677 sub outstanding_credits {
678     my ($self) = @_;
679
680     return $self->lines->search(
681         {
682             amount            => { '<' => 0 },
683             amountoutstanding => { '<' => 0 }
684         }
685     );
686 }
687
688 =head3 non_issues_charges
689
690 my $non_issues_charges = $self->non_issues_charges
691
692 Calculates amount immediately owing by the patron - non-issue charges.
693
694 Charges exempt from non-issue are:
695 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
696 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
697 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
698
699 =cut
700
701 sub non_issues_charges {
702     my ($self) = @_;
703
704     #NOTE: With bug 23049 these preferences could be moved to being attached
705     #to individual debit types to give more flexability and specificity.
706     my @not_fines;
707     push @not_fines, 'RESERVE'
708       unless C4::Context->preference('HoldsInNoissuesCharge');
709     push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
710       unless C4::Context->preference('RentalsInNoissuesCharge');
711     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
712         my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
713         push @not_fines, @man_inv;
714     }
715
716     return $self->lines->search(
717         {
718             debit_type_code => { -not_in => \@not_fines }
719         },
720     )->total_outstanding;
721 }
722
723 =head3 lines
724
725 my $lines = $self->lines;
726
727 Return all credits and debits for the user, outstanding or otherwise
728
729 =cut
730
731 sub lines {
732     my ($self) = @_;
733
734     return Koha::Account::Lines->search(
735         {
736             borrowernumber => $self->{patron_id},
737         }
738     );
739 }
740
741 =head3 reconcile_balance
742
743 $account->reconcile_balance();
744
745 Find outstanding credits and use them to pay outstanding debits.
746 Currently, this implicitly uses the 'First In First Out' rule for
747 applying credits against debits.
748
749 =cut
750
751 sub reconcile_balance {
752     my ($self) = @_;
753
754     my $outstanding_debits  = $self->outstanding_debits;
755     my $outstanding_credits = $self->outstanding_credits;
756
757     while (     $outstanding_debits->total_outstanding > 0
758             and my $credit = $outstanding_credits->next )
759     {
760         # there's both outstanding debits and credits
761         $credit->apply( { debits => [ $outstanding_debits->as_list ] } );    # applying credit, no special offset
762
763         $outstanding_debits = $self->outstanding_debits;
764
765     }
766
767     return $self;
768 }
769
770 1;
771
772 =head2 Name mappings
773
774 =head3 $offset_type
775
776 =cut
777
778 our $offset_type = {
779     'CREDIT'           => 'Manual Credit',
780     'FORGIVEN'         => 'Writeoff',
781     'LOST_FOUND'       => 'Lost Item Found',
782     'OVERPAYMENT'      => 'Overpayment',
783     'PAYMENT'          => 'Payment',
784     'WRITEOFF'         => 'Writeoff',
785     'ACCOUNT'          => 'Account Fee',
786     'ACCOUNT_RENEW'    => 'Account Fee',
787     'RESERVE'          => 'Reserve Fee',
788     'PROCESSING'       => 'Processing Fee',
789     'LOST'             => 'Lost Item',
790     'RENT'             => 'Rental Fee',
791     'RENT_DAILY'       => 'Rental Fee',
792     'RENT_RENEW'       => 'Rental Fee',
793     'RENT_DAILY_RENEW' => 'Rental Fee',
794     'OVERDUE'          => 'OVERDUE',
795     'RESERVE_EXPIRED'  => 'Hold Expired',
796     'PAYOUT'           => 'PAYOUT',
797 };
798
799 =head1 AUTHORS
800
801 =encoding utf8
802
803 Kyle M Hall <kyle.m.hall@gmail.com>
804 Tomás Cohen Arazi <tomascohen@gmail.com>
805 Martin Renvoize <martin.renvoize@ptfs-europe.com>
806
807 =cut