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