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