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