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