From 78826182687b2241728e05307c24da76390f3d1b Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Thu, 21 Nov 2019 12:10:41 +0000 Subject: [PATCH] Bug 23442: Add 'reduce' method to Koha::Account::Line This enhancement adds a 'reduce' method to Koha::Account::Line which can be used to reduce a charge/debit by adding a credit to offset against the amount outstanding. It may be used to apply a discount whilst retaining the original debit amounts or to apply a full or partial refund for example when a lost item is found and returned. The created credit will be immediately applied against the debit unless the debit has already been paid, in which case a 'zero' offset will be added to maintain a link to the debit but the outstanding credit will be left so it may be applied to other debts. Test Plan: 1) Run the included tests and verify they pass. 2) Signoff --- Koha/Account/Line.pm | 135 +++++++++++++++++++++++ t/db_dependent/Koha/Account/Lines.t | 160 +++++++++++++++++++++++++++- 2 files changed, 294 insertions(+), 1 deletion(-) diff --git a/Koha/Account/Line.pm b/Koha/Account/Line.pm index 5585681623..02efc741fd 100644 --- a/Koha/Account/Line.pm +++ b/Koha/Account/Line.pm @@ -270,6 +270,141 @@ sub void { } +=head3 reduce + + $charge_accountline->reduce({ + reduction_type => $reduction_type + }); + +Used to 'reduce' a charge/debit by adding a credit to offset against the amount +outstanding. + +May be used to apply a discount whilst retaining the original debit amounts or +to apply a full or partial refund for example when a lost item is found and +returned. + +It will immediately be applied to the given debit unless the debit has already +been paid, in which case a 'zero' offset will be added to maintain a link to +the debit but the outstanding credit will be left so it may be applied to other +debts. + +Reduction type may be one of: + +* DISCOUNT +* REFUND + +Returns the reduction accountline (which will be a credit) + +=cut + +sub reduce { + my ( $self, $params ) = @_; + + # Make sure it is a charge we are reducing + unless ( $self->is_debit ) { + Koha::Exceptions::Account::IsNotDebit->throw( + error => 'Account line ' . $self->id . 'is not a debit' ); + } + + # Check for mandatory parameters + my @mandatory = ( 'interface', 'reduction_type', 'amount' ); + for my $param (@mandatory) { + unless ( defined( $params->{$param} ) ) { + Koha::Exceptions::MissingParameter->throw( + error => "The $param parameter is mandatory" ); + } + } + + # More mandatory parameters + if ( $params->{interface} eq 'intranet' ) { + my @optional = ( 'staff_id', 'branch' ); + for my $param (@optional) { + unless ( defined( $params->{$param} ) ) { + Koha::Exceptions::MissingParameter->throw( error => +"The $param parameter is mandatory when interface is set to 'intranet'" + ); + } + } + } + + # Make sure the reduction isn't more than the original + my $original = $self->amount; + Koha::Exceptions::Account::AmountNotPositive->throw( + error => 'Reduce amount passed is not positive' ) + unless ( $params->{amount} > 0 ); + Koha::Exceptions::ParameterTooHigh->throw( error => +"Amount to reduce ($params->{amount}) is higher than original amount ($original)" + ) unless ( $original >= $params->{amount} ); + my $reduced = + $self->credits( { credit_type_code => [ 'DISCOUNT', 'REFUND' ] } )->total; + Koha::Exceptions::ParameterTooHigh->throw( error => +"Combined reduction ($params->{amount} + $reduced) is higher than original amount (" + . abs($original) + . ")" ) + unless ( $original >= ( $params->{amount} + abs($reduced) ) ); + + my $status = { 'REFUND' => 'REFUNDED', 'DISCOUNT' => 'DISCOUNTED' }; + + my $reduction; + $self->_result->result_source->schema->txn_do( + sub { + + # A 'reduction' is a 'credit' + $reduction = Koha::Account::Line->new( + { + date => \'NOW()', + amount => 0 - $params->{amount}, + credit_type_code => $params->{reduction_type}, + status => 'ADDED', + amountoutstanding => 0 - $params->{amount}, + manager_id => $params->{staff_id}, + borrowernumber => $self->borrowernumber, + interface => $params->{interface}, + branchcode => $params->{branch}, + } + )->store(); + + my $reduction_offset = Koha::Account::Offset->new( + { + credit_id => $reduction->accountlines_id, + type => uc( $params->{reduction_type} ), + amount => $params->{amount} + } + )->store(); + + # Link reduction to charge (and apply as required) + my $debit_outstanding = $self->amountoutstanding; + if ( $debit_outstanding >= $params->{amount} ) { + + $reduction->apply( + { + debits => [$self], + offset_type => $params->{reduction_type} + } + ); + $reduction->status('APPLIED')->store(); + } + else { + + # Zero amount offset used to link original 'debit' to reduction 'credit' + my $link_reduction_offset = Koha::Account::Offset->new( + { + credit_id => $reduction->accountlines_id, + debit_id => $self->accountlines_id, + type => $params->{reduction_type}, + amount => 0 + } + )->store(); + } + + # Update status of original debit + $self->status( $status->{ $params->{reduction_type} } )->store; + } + ); + + return $reduction->discard_changes; +} + =head3 apply my $debits = $account->outstanding_debits; diff --git a/t/db_dependent/Koha/Account/Lines.t b/t/db_dependent/Koha/Account/Lines.t index f0601354f8..873cb38739 100755 --- a/t/db_dependent/Koha/Account/Lines.t +++ b/t/db_dependent/Koha/Account/Lines.t @@ -19,7 +19,7 @@ use Modern::Perl; -use Test::More tests => 14; +use Test::More tests => 15; use Test::Exception; use C4::Circulation qw/AddIssue AddReturn/; @@ -1029,4 +1029,162 @@ subtest "payout() tests" => sub { $schema->storage->txn_rollback; }; +subtest "reduce() tests" => sub { + + plan tests => 25; + + $schema->storage->txn_begin; + + # Create a borrower + my $categorycode = + $builder->build( { source => 'Category' } )->{categorycode}; + my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode}; + + my $borrower = Koha::Patron->new( + { + cardnumber => 'dariahall', + surname => 'Hall', + firstname => 'Daria', + } + ); + $borrower->categorycode($categorycode); + $borrower->branchcode($branchcode); + $borrower->store; + + my $staff = Koha::Patron->new( + { + cardnumber => 'bobby', + surname => 'Bloggs', + firstname => 'Bobby', + } + ); + $staff->categorycode($categorycode); + $staff->branchcode($branchcode); + $staff->store; + + my $account = Koha::Account->new( { patron_id => $borrower->id } ); + + my $debit1 = Koha::Account::Line->new( + { + borrowernumber => $borrower->borrowernumber, + amount => 20, + amountoutstanding => 20, + interface => 'commandline', + debit_type_code => 'LOST' + } + )->store(); + my $credit1 = Koha::Account::Line->new( + { + borrowernumber => $borrower->borrowernumber, + amount => -20, + amountoutstanding => -20, + interface => 'commandline', + credit_type_code => 'CREDIT' + } + )->store(); + + is( $account->balance(), 0, "Account balance is 0" ); + is( $debit1->amountoutstanding, + 20, 'Overdue fee has an amount outstanding of 20' ); + is( $credit1->amountoutstanding, + -20, 'Credit has an amount outstanding of -20' ); + + my $reduce_params = { + interface => 'commandline', + reduction_type => 'REFUND', + amount => 5, + staff_id => $staff->borrowernumber, + branch => $branchcode + }; + + throws_ok { $credit1->reduce($reduce_params); } + 'Koha::Exceptions::Account::IsNotDebit', + '->reduce() can only be used with debits'; + + my @required = ( 'interface', 'reduction_type', 'amount' ); + for my $required (@required) { + my $params = {%$reduce_params}; + delete( $params->{$required} ); + throws_ok { + $debit1->reduce($params); + } + 'Koha::Exceptions::MissingParameter', + "->reduce() requires the `$required` parameter is passed"; + } + + $reduce_params->{interface} = 'intranet'; + my @dependant_required = ( 'staff_id', 'branch' ); + for my $d (@dependant_required) { + my $params = {%$reduce_params}; + delete( $params->{$d} ); + throws_ok { + $debit1->reduce($params); + } + 'Koha::Exceptions::MissingParameter', +"->reduce() requires the `$d` parameter is passed when interface is intranet"; + } + + throws_ok { + $debit1->reduce( + { + interface => 'intranet', + staff_id => $staff->borrowernumber, + branch => $branchcode, + reduction_type => 'REFUND', + amount => 25 + } + ); + } + 'Koha::Exceptions::ParameterTooHigh', + '->reduce() cannot reduce more than original amount'; + + # Partial Reduction + # (Refund 5 on debt of 20) + my $reduction = $debit1->reduce($reduce_params); + + is( $reduction->amount() * 1, -5, "Reduce amount is -5" ); + is( $reduction->amountoutstanding() * 1, + 0, "Reduce amountoutstanding is 0" ); + is( $debit1->amountoutstanding() * 1, + 15, "Debit amountoutstanding reduced by 5 to 15" ); + is( $account->balance() * 1, -5, "Account balance is -5" ); + is( $reduction->status(), 'APPLIED', "Reduction status is 'APPLIED'" ); + + my $offsets = Koha::Account::Offsets->search( + { credit_id => $reduction->id, debit_id => $debit1->id } ); + is( $offsets->count, 1, 'Only one offset is generated' ); + my $THE_offset = $offsets->next; + is( $THE_offset->amount * 1, + -5, 'Correct amount was applied against debit' ); + is( $THE_offset->type, 'REFUND', "Offset type set to 'REFUND'" ); + + # Zero offset created when zero outstanding + # (Refund another 5 on paid debt of 20) + $credit1->apply( { debits => [ $debit1 ] } ); + is($debit1->amountoutstanding + 0, 0, 'Debit1 amountoutstanding reduced to 0'); + $reduction = $debit1->reduce($reduce_params); + is( $reduction->amount() * 1, -5, "Reduce amount is -5" ); + is( $reduction->amountoutstanding() * 1, + -5, "Reduce amountoutstanding is -5" ); + + $offsets = Koha::Account::Offsets->search( + { credit_id => $reduction->id, debit_id => $debit1->id } ); + is( $offsets->count, 1, 'Only one new offset is generated' ); + $THE_offset = $offsets->next; + is( $THE_offset->amount * 1, + 0, 'Zero offset created for already paid off debit' ); + is( $THE_offset->type, 'REFUND', "Offset type set to 'REFUND'" ); + + # Compound reduction should not allow more than original amount + # (Reduction of 5 + 5 + 20 > 20) + $reduce_params->{amount} = 20; + throws_ok { + $debit1->reduce($reduce_params); + } + 'Koha::Exceptions::ParameterTooHigh', +'->reduce cannot reduce mor than the original amount (combined reductions test)'; + + $schema->storage->txn_rollback; +}; + 1; -- 2.20.1