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