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