FINERACT-2080: Move loan validation from entity to validator classes
This commit is contained in:
committed by
Adam Saghy
parent
56ee2eaaff
commit
a2c01d25e5
+64
-1927
File diff suppressed because it is too large
Load Diff
-1
@@ -448,7 +448,6 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
}
|
||||
|
||||
public void reverse() {
|
||||
this.loan.validateRepaymentTypeTransactionNotBeforeAChargeRefund(this, "reversed");
|
||||
this.reversed = true;
|
||||
this.reversedOnDate = DateUtils.getBusinessLocalDate();
|
||||
this.loanTransactionToRepaymentScheduleMappings.clear();
|
||||
|
||||
+3
@@ -50,6 +50,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymen
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
@@ -64,6 +65,7 @@ import org.springframework.util.CollectionUtils;
|
||||
public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implements LoanRepaymentScheduleTransactionProcessor {
|
||||
|
||||
public final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
|
||||
public final LoanChargeValidator loanChargeValidator = new LoanChargeValidator();
|
||||
|
||||
@Override
|
||||
public boolean accept(String s) {
|
||||
@@ -501,6 +503,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
||||
|
||||
protected void createNewTransaction(LoanTransaction loanTransaction, LoanTransaction newLoanTransaction,
|
||||
ChangedTransactionDetail changedTransactionDetail) {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, "reversed");
|
||||
loanTransaction.reverse();
|
||||
loanTransaction.updateExternalId(null);
|
||||
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.serialization;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeAddedException;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public final class LoanChargeValidator {
|
||||
|
||||
public void validateLoanIsNotClosed(final Loan loan, final LoanCharge loanCharge) {
|
||||
if (loan.isClosed()) {
|
||||
final String defaultUserMessage = "This charge cannot be added as the loan is already closed.";
|
||||
throw new LoanChargeCannotBeAddedException("loanCharge", "loan.is.closed", defaultUserMessage, loan.getId(), loanCharge.name());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: to remove this constraint requires that loan transactions
|
||||
// that represent the waive of charges also be removed (or reversed)M
|
||||
// if you want ability to remove loan charges that are waived.
|
||||
public void validateLoanChargeIsNotWaived(final Loan loan, final LoanCharge loanCharge) {
|
||||
if (loanCharge.isWaived()) {
|
||||
final String defaultUserMessage = "This loan charge cannot be removed as the charge as already been waived.";
|
||||
throw new LoanChargeCannotBeAddedException("loanCharge", "loanCharge.is.waived", defaultUserMessage, loan.getId(),
|
||||
loanCharge.name());
|
||||
}
|
||||
}
|
||||
|
||||
public void validateChargeHasValidSpecifiedDateIfApplicable(final Loan loan, final LoanCharge loanCharge,
|
||||
final LocalDate disbursementDate) {
|
||||
if (loanCharge.isSpecifiedDueDate() && DateUtils.isBefore(loanCharge.getDueLocalDate(), disbursementDate)) {
|
||||
final String defaultUserMessage = "This charge with specified due date cannot be added as the it is not in schedule range.";
|
||||
throw new LoanChargeCannotBeAddedException("loanCharge", "specified.due.date.outside.range", defaultUserMessage,
|
||||
loan.getDisbursementDate(), loanCharge.name());
|
||||
}
|
||||
}
|
||||
|
||||
public void validateChargeAdditionForDisbursedLoan(final Loan loan, final LoanCharge loanCharge) {
|
||||
if (loan.isChargesAdditionAllowed() && loanCharge.isDueAtDisbursement()) {
|
||||
// Note: added this constraint to restrict adding disbursement
|
||||
// charges to a loan
|
||||
// after it is disbursed
|
||||
// if the loan charge payment type is 'Disbursement'.
|
||||
// To undo this constraint would mean resolving how charges due are
|
||||
// disbursement are handled at present.
|
||||
// When a loan is disbursed and has charges due at disbursement, a
|
||||
// transaction is created to auto record
|
||||
// payment of the charges (user has no choice in saying they were or
|
||||
// were not paid) - so its assumed they were paid.
|
||||
final String defaultUserMessage = "This charge which is due at disbursement cannot be added as the loan is already disbursed.";
|
||||
throw new LoanChargeCannotBeAddedException("loanCharge", "due.at.disbursement.and.loan.is.disbursed", defaultUserMessage,
|
||||
loan.getId(), loanCharge.name());
|
||||
}
|
||||
}
|
||||
|
||||
public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final Loan loan, final LoanTransaction repaymentTransaction,
|
||||
final String reversedOrCreated) {
|
||||
if (repaymentTransaction.isRepaymentLikeType() && !repaymentTransaction.isChargeRefund()) {
|
||||
for (LoanTransaction txn : loan.getLoanTransactions()) {
|
||||
if (txn.isChargeRefund() && DateUtils.isBefore(repaymentTransaction.getTransactionDate(), txn.getTransactionDate())) {
|
||||
final String errorMessage = "loan.transaction.cant.be." + reversedOrCreated + ".because.later.charge.refund.exists";
|
||||
final String details = "Loan Transaction: " + loan.getId() + " Can't be " + reversedOrCreated
|
||||
+ " because a Later Charge Refund Exists.";
|
||||
throw new LoanChargeRefundException(errorMessage, details);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void validateChargePaymentNotInFuture(final LoanTransaction paymentTransaction) {
|
||||
if (DateUtils.isDateInTheFuture(paymentTransaction.getTransactionDate())) {
|
||||
final String errorMessage = "The date on which a loan charge paid cannot be in the future.";
|
||||
throw new InvalidLoanStateTransitionException("charge.payment", "cannot.be.a.future.date", errorMessage,
|
||||
paymentTransaction.getTransactionDate());
|
||||
}
|
||||
}
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.serialization;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.organisation.holiday.domain.Holiday;
|
||||
import org.apache.fineract.organisation.holiday.service.HolidayUtil;
|
||||
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
|
||||
import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public final class LoanDownPaymentTransactionValidator {
|
||||
|
||||
public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays,
|
||||
final boolean allowTransactionsOnNonWorkingDay) {
|
||||
if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) {
|
||||
final String errorMessage = "Repayment date cannot be on a non working day";
|
||||
throw new LoanApplicationDateException("repayment.date.on.non.working.day", errorMessage, repaymentDate);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateRepaymentDateIsOnHoliday(final LocalDate repaymentDate, final boolean allowTransactionsOnHoliday,
|
||||
final List<Holiday> holidays) {
|
||||
if (!allowTransactionsOnHoliday && HolidayUtil.isHoliday(repaymentDate, holidays)) {
|
||||
final String errorMessage = "Repayment date cannot be on a holiday";
|
||||
throw new LoanApplicationDateException("repayment.date.on.holiday", errorMessage, repaymentDate);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateLoanStatusIsActiveOrFullyPaidOrOverpaid(final Loan loan) {
|
||||
if (!(loan.isOpen() || loan.isClosedObligationsMet() || loan.isOverPaid())) {
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final String defaultUserMessage = "Loan must be Active, Fully Paid or Overpaid";
|
||||
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.must.be.active.fully.paid.or.overpaid",
|
||||
defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
throw new PlatformApiDataValidationException(dataValidationErrors);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateRepaymentTypeAccountStatus(final Loan loan, final LoanTransaction repaymentTransaction, final LoanEvent event) {
|
||||
if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isInterestPaymentWaiver()
|
||||
|| repaymentTransaction.isMerchantIssuedRefund() || repaymentTransaction.isPayoutRefund()
|
||||
|| repaymentTransaction.isChargeRefund() || repaymentTransaction.isRepayment() || repaymentTransaction.isDownPayment()
|
||||
|| repaymentTransaction.isInterestRefund()) {
|
||||
validateLoanStatusIsActiveOrFullyPaidOrOverpaid(loan);
|
||||
} else {
|
||||
validateAccountStatus(loan, event);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateAccountStatus(final Loan loan, final LoanEvent event) {
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
|
||||
switch (event) {
|
||||
case LOAN_APPROVED -> {
|
||||
if (!loan.isSubmittedAndPendingApproval()) {
|
||||
final String defaultUserMessage = "Loan Account Approval is not allowed. Loan Account is not in submitted and pending approval state.";
|
||||
final ApiParameterError error = ApiParameterError
|
||||
.generalError("error.msg.loan.approve.account.is.not.submitted.and.pending.state", defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
}
|
||||
case LOAN_APPROVAL_UNDO -> {
|
||||
if (!loan.isApproved()) {
|
||||
final String defaultUserMessage = "Loan Account Undo Approval is not allowed. Loan Account is not in approved state.";
|
||||
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.undo.approval.account.is.not.approved",
|
||||
defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
}
|
||||
case LOAN_DISBURSED -> {
|
||||
if ((!(loan.isApproved() && loan.isNotDisbursed()) && !loan.getLoanProduct().isMultiDisburseLoan())
|
||||
|| (loan.getLoanProduct().isMultiDisburseLoan() && !loan.isAllTranchesNotDisbursed())) {
|
||||
final String defaultUserMessage = "Loan Disbursal is not allowed. Loan Account is not in approved and not disbursed state.";
|
||||
final ApiParameterError error = ApiParameterError
|
||||
.generalError("error.msg.loan.disbursal.account.is.not.approve.not.disbursed.state", defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
}
|
||||
case LOAN_DISBURSAL_UNDO -> {
|
||||
if (!loan.isOpen()) {
|
||||
final String defaultUserMessage = "Loan Undo disbursal is not allowed. Loan Account is not active.";
|
||||
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.undo.disbursal.account.is.not.active",
|
||||
defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
if (loan.isOpen() && loan.isTopup()) {
|
||||
final String defaultUserMessage = "Loan Undo disbursal is not allowed on Topup Loans";
|
||||
final ApiParameterError error = ApiParameterError
|
||||
.generalError("error.msg.loan.undo.disbursal.not.allowed.on.topup.loan", defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
}
|
||||
case LOAN_REPAYMENT_OR_WAIVER -> {
|
||||
if (!loan.isOpen()) {
|
||||
final String defaultUserMessage = "Loan Repayment (or its types) or Waiver is not allowed. Loan Account is not active.";
|
||||
final ApiParameterError error = ApiParameterError
|
||||
.generalError("error.msg.loan.repayment.or.waiver.account.is.not.active", defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
}
|
||||
case WRITE_OFF_OUTSTANDING -> {
|
||||
if (!loan.isOpen()) {
|
||||
final String defaultUserMessage = "Loan Written off is not allowed. Loan Account is not active.";
|
||||
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.writtenoff.account.is.not.active",
|
||||
defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
}
|
||||
case WRITE_OFF_OUTSTANDING_UNDO -> {
|
||||
if (!loan.isClosedWrittenOff()) {
|
||||
final String defaultUserMessage = "Loan Undo Written off is not allowed. Loan Account is not Written off.";
|
||||
final ApiParameterError error = ApiParameterError
|
||||
.generalError("error.msg.loan.undo.writtenoff.account.is.not.written.off", defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
}
|
||||
case LOAN_CHARGE_PAYMENT -> {
|
||||
if (!loan.isOpen()) {
|
||||
final String defaultUserMessage = "Charge payment is not allowed. Loan Account is not Active.";
|
||||
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.charge.payment.account.is.not.active",
|
||||
defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
}
|
||||
case LOAN_CLOSED -> {
|
||||
if (!loan.isOpen()) {
|
||||
final String defaultUserMessage = "Closing Loan Account is not allowed. Loan Account is not Active.";
|
||||
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.close.account.is.not.active",
|
||||
defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
}
|
||||
case LOAN_EDIT_MULTI_DISBURSE_DATE -> {
|
||||
if (loan.isClosed()) {
|
||||
final String defaultUserMessage = "Edit disbursement is not allowed. Loan Account is not active.";
|
||||
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.edit.disbursement.account.is.not.active",
|
||||
defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
}
|
||||
case LOAN_RECOVERY_PAYMENT -> {
|
||||
if (!loan.isClosedWrittenOff()) {
|
||||
final String defaultUserMessage = "Recovery repayments may only be made on loans which are written off";
|
||||
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.account.is.not.written.off",
|
||||
defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
}
|
||||
case LOAN_REFUND -> {
|
||||
if (!loan.isOpen()) {
|
||||
final String defaultUserMessage = "Loan Refund is not allowed. Loan Account is not active.";
|
||||
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.refund.account.is.not.active",
|
||||
defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
}
|
||||
case LOAN_DISBURSAL_UNDO_LAST -> {
|
||||
if (!loan.isOpen()) {
|
||||
final String defaultUserMessage = "Loan Undo last disbursal is not allowed. Loan Account is not active.";
|
||||
final ApiParameterError error = ApiParameterError
|
||||
.generalError("error.msg.loan.undo.last.disbursal.account.is.not.active", defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
}
|
||||
case LOAN_FORECLOSURE -> {
|
||||
if (!loan.isOpen()) {
|
||||
final String defaultUserMessage = "Loan foreclosure is not allowed. Loan Account is not active.";
|
||||
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.foreclosure.account.is.not.active",
|
||||
defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
}
|
||||
case LOAN_CREDIT_BALANCE_REFUND -> {
|
||||
if (!loan.getStatus().isOverpaid()) {
|
||||
final String defaultUserMessage = "Loan Credit Balance Refund is not allowed. Loan Account is not Overpaid.";
|
||||
final ApiParameterError error = ApiParameterError
|
||||
.generalError("error.msg.loan.credit.balance.refund.account.is.not.overpaid", defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
}
|
||||
case LOAN_CHARGE_ADJUSTMENT -> {
|
||||
if (!(loan.getStatus().isActive() || loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid())) {
|
||||
final String defaultUserMessage = "Loan Charge Adjustment is not allowed. Loan Account must be either Active, Fully repaid or Overpaid.";
|
||||
final ApiParameterError error = ApiParameterError
|
||||
.generalError("error.msg.loan.charge.adjustment.account.is.not.in.valid.state", defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException(dataValidationErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.serialization;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public final class LoanRefundValidator {
|
||||
|
||||
public void validateTransferRefund(final Loan loan, final LoanTransaction loanTransaction) {
|
||||
if (loan.getStatus().isOverpaid()) {
|
||||
if (loan.getTotalOverpaid().compareTo(loanTransaction.getAmount(loan.getCurrency()).getAmount()) < 0) {
|
||||
final String errorMessage = "The refund amount must be less than or equal to overpaid amount ";
|
||||
throw new InvalidLoanStateTransitionException("transaction", "is.exceeding.overpaid.amount", errorMessage,
|
||||
loan.getTotalOverpaid(), loanTransaction.getAmount(loan.getCurrency()).getAmount());
|
||||
} else if (!loan.isAfterLastRepayment(loanTransaction, loan.getLoanTransactions())) {
|
||||
final String errorMessage = "Transfer funds is allowed only after last repayment date";
|
||||
throw new InvalidLoanStateTransitionException("transaction", "is.not.after.repayment.date", errorMessage);
|
||||
}
|
||||
} else {
|
||||
final String errorMessage = "Transfer funds is allowed only for loan accounts with overpaid status ";
|
||||
throw new InvalidLoanStateTransitionException("transaction", "is.not.a.overpaid.loan", errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateTransactionDateAfterDisbursement(final Loan loan, final LocalDate loanTransactionDate) {
|
||||
if (DateUtils.isBefore(loanTransactionDate, loan.getDisbursementDate())) {
|
||||
final String errorMessage = "The transaction date cannot be before the loan disbursement date: "
|
||||
+ loan.getDisbursementDate().toString();
|
||||
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.before.disbursement.date", errorMessage,
|
||||
loanTransactionDate, loan.getDisbursementDate());
|
||||
}
|
||||
}
|
||||
|
||||
public void validateCreditBalanceRefund(final Loan loan, final LoanTransaction newCreditBalanceRefundTransaction) {
|
||||
if (!newCreditBalanceRefundTransaction.isGreaterThanZeroAndLessThanOrEqualTo(loan.getTotalOverpaid())) {
|
||||
final String errorMessage = "Transaction Amount ("
|
||||
+ newCreditBalanceRefundTransaction.getAmount(loan.getCurrency()).getAmount().toString()
|
||||
+ ") must be > zero and <= Overpaid amount (" + loan.getTotalOverpaid().toString() + ").";
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final ApiParameterError error = ApiParameterError.parameterError(
|
||||
"error.msg.transactionAmount.invalid.must.be.>zero.and<=overpaidamount", errorMessage, "transactionAmount",
|
||||
newCreditBalanceRefundTransaction.getAmount(loan.getCurrency()));
|
||||
dataValidationErrors.add(error);
|
||||
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
dataValidationErrors);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateRefundEligibility(final Loan loan, final LoanTransaction loanTransaction) {
|
||||
if (loan.getStatus().isOverpaid() || loan.getStatus().isClosed()) {
|
||||
final String errorMessage = "This refund option is only for active loans ";
|
||||
throw new InvalidLoanStateTransitionException("transaction", "is.exceeding.overpaid.amount", errorMessage,
|
||||
loan.getTotalOverpaid(), loanTransaction.getAmount(loan.getCurrency()).getAmount());
|
||||
} else if (loan.getTotalPaidInRepayments().isZero()) {
|
||||
final String errorMessage = "Cannot refund when no payment has been made";
|
||||
throw new InvalidLoanStateTransitionException("transaction", "no.payment.yet.made.for.loan", errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateRefundTransactionType(final LoanTransaction loanTransaction) {
|
||||
if (loanTransaction.isNotRefundForActiveLoan()) {
|
||||
final String errorMessage = "A transaction of type refund was expected but not received.";
|
||||
throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.refund.transaction", errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateTransactionDateNotInFuture(final LocalDate transactionDate) {
|
||||
if (DateUtils.isDateInTheFuture(transactionDate)) {
|
||||
final String errorMessage = "The transaction date cannot be in the future.";
|
||||
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.a.future.date", errorMessage, transactionDate);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateTransactionAmountThreshold(final Loan loan, final LoanTransaction adjustedTransaction) {
|
||||
if (loan.getLoanProduct().isMultiDisburseLoan() && adjustedTransaction == null) {
|
||||
final BigDecimal totalDisbursed = loan.getDisbursedAmount();
|
||||
final BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
|
||||
final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted);
|
||||
if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0) {
|
||||
final String errorMessage = "The transaction amount cannot exceed threshold.";
|
||||
throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class LoanChargeService {
|
||||
|
||||
private final LoanChargeValidator loanChargeValidator;
|
||||
|
||||
public void recalculateAllCharges(final Loan loan) {
|
||||
Set<LoanCharge> charges = loan.getActiveCharges();
|
||||
int penaltyWaitPeriod = 0;
|
||||
for (final LoanCharge loanCharge : charges) {
|
||||
recalculateLoanCharge(loan, loanCharge, penaltyWaitPeriod);
|
||||
}
|
||||
loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement());
|
||||
}
|
||||
|
||||
public void recalculateLoanCharge(final Loan loan, final LoanCharge loanCharge, final int penaltyWaitPeriod) {
|
||||
BigDecimal amount = BigDecimal.ZERO;
|
||||
BigDecimal chargeAmt;
|
||||
BigDecimal totalChargeAmt = BigDecimal.ZERO;
|
||||
if (loanCharge.getChargeCalculation().isPercentageBased()) {
|
||||
if (loanCharge.isOverdueInstallmentCharge()) {
|
||||
amount = loan.calculateOverdueAmountPercentageAppliedTo(loanCharge, penaltyWaitPeriod);
|
||||
} else {
|
||||
amount = loan.calculateAmountPercentageAppliedTo(loanCharge);
|
||||
}
|
||||
chargeAmt = loanCharge.getPercentage();
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
totalChargeAmt = loan.calculatePerInstallmentChargeAmount(loanCharge);
|
||||
}
|
||||
} else {
|
||||
chargeAmt = loanCharge.amountOrPercentage();
|
||||
}
|
||||
if (loanCharge.isActive()) {
|
||||
loan.clearLoanInstallmentChargesBeforeRegeneration(loanCharge);
|
||||
loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(),
|
||||
totalChargeAmt);
|
||||
loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate());
|
||||
}
|
||||
}
|
||||
|
||||
public void makeChargePayment(final Loan loan, final Long chargeId, final LoanLifecycleStateMachine loanLifecycleStateMachine,
|
||||
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
|
||||
final LoanTransaction paymentTransaction, final Integer installmentNumber) {
|
||||
loanChargeValidator.validateChargePaymentNotInFuture(paymentTransaction);
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
LoanCharge charge = null;
|
||||
for (final LoanCharge loanCharge : loan.getCharges()) {
|
||||
if (loanCharge.isActive() && chargeId.equals(loanCharge.getId())) {
|
||||
charge = loanCharge;
|
||||
}
|
||||
}
|
||||
loan.handleChargePaidTransaction(charge, paymentTransaction, loanLifecycleStateMachine, installmentNumber);
|
||||
}
|
||||
}
|
||||
+6
@@ -20,11 +20,17 @@ package org.apache.fineract.portfolio.loanaccount.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
|
||||
public interface LoanDownPaymentHandlerService {
|
||||
|
||||
LoanTransaction handleDownPayment(ScheduleGeneratorDTO scheduleGeneratorDTO, JsonCommand command,
|
||||
LoanTransaction disbursementTransaction, Loan loan);
|
||||
|
||||
ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(Loan loan, LoanTransaction newTransactionDetail,
|
||||
LoanLifecycleStateMachine loanLifecycleStateMachine, LoanTransaction transactionForAdjustment,
|
||||
ScheduleGeneratorDTO scheduleGeneratorDTO);
|
||||
}
|
||||
|
||||
+188
-1
@@ -18,17 +18,39 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.service;
|
||||
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.ACTUAL_DISBURSEMENT_DATE;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionDownPaymentPostBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionDownPaymentPreBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanRefundValidator;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -36,12 +58,16 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
|
||||
|
||||
private final LoanTransactionRepository loanTransactionRepository;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator;
|
||||
private final LoanScheduleService loanScheduleService;
|
||||
private final LoanRefundService loanRefundService;
|
||||
private final LoanRefundValidator loanRefundValidator;
|
||||
|
||||
@Override
|
||||
public LoanTransaction handleDownPayment(ScheduleGeneratorDTO scheduleGeneratorDTO, JsonCommand command,
|
||||
LoanTransaction disbursementTransaction, Loan loan) {
|
||||
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionDownPaymentPreBusinessEvent(loan));
|
||||
LoanTransaction downPaymentTransaction = loan.handleDownPayment(disbursementTransaction, command, scheduleGeneratorDTO);
|
||||
LoanTransaction downPaymentTransaction = handleDownPayment(loan, disbursementTransaction, command, scheduleGeneratorDTO);
|
||||
if (downPaymentTransaction != null) {
|
||||
downPaymentTransaction = loanTransactionRepository.saveAndFlush(downPaymentTransaction);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanTransactionDownPaymentPostBusinessEvent(downPaymentTransaction));
|
||||
@@ -49,4 +75,165 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
|
||||
}
|
||||
return downPaymentTransaction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(final Loan loan, final LoanTransaction loanTransaction,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction,
|
||||
final ScheduleGeneratorDTO scheduleGeneratorDTO) {
|
||||
ChangedTransactionDetail changedTransactionDetail = null;
|
||||
|
||||
if (loanTransaction.isRecoveryRepayment()) {
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_RECOVERY_PAYMENT, loan);
|
||||
}
|
||||
|
||||
if (loanTransaction.isRecoveryRepayment()
|
||||
&& loanTransaction.getAmount(loan.getCurrency()).getAmount().compareTo(loan.getSummary().getTotalWrittenOff()) > 0) {
|
||||
final String errorMessage = "The transaction amount cannot greater than the remaining written off amount.";
|
||||
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.greater.than.total.written.off", errorMessage);
|
||||
}
|
||||
|
||||
loanTransaction.updateLoan(loan);
|
||||
|
||||
final boolean isTransactionChronologicallyLatest = loan.isChronologicallyLatestRepaymentOrWaiver(loanTransaction);
|
||||
|
||||
if (loanTransaction.isNotZero()) {
|
||||
loan.addLoanTransaction(loanTransaction);
|
||||
}
|
||||
|
||||
if (loanTransaction.isNotRepaymentLikeType() && loanTransaction.isNotWaiver() && loanTransaction.isNotRecoveryRepayment()) {
|
||||
final String errorMessage = "A transaction of type repayment or recovery repayment or waiver was expected but not received.";
|
||||
throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.repayment.or.waiver.or.recovery.transaction",
|
||||
errorMessage);
|
||||
}
|
||||
|
||||
final LocalDate loanTransactionDate = loanRefundService.extractTransactionDate(loan, loanTransaction);
|
||||
|
||||
if (DateUtils.isDateInTheFuture(loanTransactionDate)) {
|
||||
final String errorMessage = "The transaction date cannot be in the future.";
|
||||
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.a.future.date", errorMessage, loanTransactionDate);
|
||||
}
|
||||
|
||||
if (loanTransaction.isInterestWaiver()) {
|
||||
Money totalInterestOutstandingOnLoan = loan.getTotalInterestOutstandingOnLoan();
|
||||
if (adjustedTransaction != null) {
|
||||
totalInterestOutstandingOnLoan = totalInterestOutstandingOnLoan.plus(adjustedTransaction.getAmount(loan.loanCurrency()));
|
||||
}
|
||||
if (loanTransaction.getAmount(loan.getCurrency()).isGreaterThan(totalInterestOutstandingOnLoan)) {
|
||||
final String errorMessage = "The amount of interest to waive cannot be greater than total interest outstanding on loan.";
|
||||
throw new InvalidLoanStateTransitionException("waive.interest", "amount.exceeds.total.outstanding.interest", errorMessage,
|
||||
loanTransaction.getAmount(loan.getCurrency()), totalInterestOutstandingOnLoan.getAmount());
|
||||
}
|
||||
}
|
||||
|
||||
loanRefundValidator.validateTransactionAmountThreshold(loan, adjustedTransaction);
|
||||
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessor();
|
||||
|
||||
final LoanRepaymentScheduleInstallment currentInstallment = loan
|
||||
.fetchLoanRepaymentScheduleInstallmentByDueDate(loanTransaction.getTransactionDate());
|
||||
|
||||
boolean reprocess = loan.isForeclosure() || !isTransactionChronologicallyLatest || adjustedTransaction != null
|
||||
|| !DateUtils.isEqualBusinessDate(loanTransaction.getTransactionDate()) || currentInstallment == null
|
||||
|| !currentInstallment.getTotalOutstanding(loan.getCurrency()).isEqualTo(loanTransaction.getAmount(loan.getCurrency()));
|
||||
|
||||
if (isTransactionChronologicallyLatest && adjustedTransaction == null
|
||||
&& (!reprocess || !loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) && !loan.isForeclosure()) {
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction,
|
||||
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
|
||||
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
|
||||
reprocess = false;
|
||||
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) {
|
||||
reprocess = true;
|
||||
} else {
|
||||
final LoanRepaymentScheduleInstallment nextInstallment = loan
|
||||
.fetchRepaymentScheduleInstallment(currentInstallment.getInstallmentNumber() + 1);
|
||||
if (nextInstallment != null && nextInstallment.getTotalPaidInAdvance(loan.getCurrency()).isGreaterThanZero()) {
|
||||
reprocess = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reprocess) {
|
||||
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()
|
||||
&& !loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)) {
|
||||
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
|
||||
}
|
||||
changedTransactionDetail = loan.reprocessTransactions();
|
||||
}
|
||||
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
|
||||
/**
|
||||
* FIXME: Vishwas, skipping post loan transaction checks for Loan recoveries
|
||||
**/
|
||||
if (loanTransaction.isNotRecoveryRepayment()) {
|
||||
loan.doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
|
||||
}
|
||||
|
||||
if (loan.getLoanProduct().isMultiDisburseLoan()) {
|
||||
final BigDecimal totalDisbursed = loan.getDisbursedAmount();
|
||||
final BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
|
||||
final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted);
|
||||
if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0
|
||||
&& loan.repaymentScheduleDetail().getPrincipal().minus(totalDisbursed).isGreaterThanZero()) {
|
||||
final String errorMessage = "The transaction amount cannot exceed threshold.";
|
||||
throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
return changedTransactionDetail;
|
||||
}
|
||||
|
||||
private LoanTransaction handleDownPayment(final Loan loan, final LoanTransaction disbursementTransaction, final JsonCommand command,
|
||||
final ScheduleGeneratorDTO scheduleGeneratorDTO) {
|
||||
final LocalDate disbursedOn = command.localDateValueOfParameterNamed(ACTUAL_DISBURSEMENT_DATE);
|
||||
final BigDecimal disbursedAmountPercentageForDownPayment = loan.getLoanRepaymentScheduleDetail()
|
||||
.getDisbursedAmountPercentageForDownPayment();
|
||||
ExternalId externalId = ExternalId.empty();
|
||||
if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) {
|
||||
externalId = ExternalId.generate();
|
||||
}
|
||||
Money downPaymentMoney = Money.of(loan.getCurrency(),
|
||||
MathUtil.percentageOf(disbursementTransaction.getAmount(), disbursedAmountPercentageForDownPayment, 19));
|
||||
if (loan.getLoanProduct().getInstallmentAmountInMultiplesOf() != null) {
|
||||
downPaymentMoney = Money.roundToMultiplesOf(downPaymentMoney, loan.getLoanProduct().getInstallmentAmountInMultiplesOf());
|
||||
}
|
||||
final Money adjustedDownPaymentMoney = switch (loan.getLoanProductRelatedDetail().getLoanScheduleType()) {
|
||||
// For Cumulative loan: To check whether the loan was overpaid when the disbursement happened and to get the
|
||||
// proper amount after the disbursement we are using two balances:
|
||||
// 1. Whether the loan is still overpaid after the disbursement,
|
||||
// 2. if the loan is not overpaid anymore after the disbursement, but was it more overpaid than the
|
||||
// calculated down-payment amount?
|
||||
case CUMULATIVE -> {
|
||||
if (loan.getTotalOverpaidAsMoney().isGreaterThanZero()) {
|
||||
yield Money.zero(loan.getCurrency());
|
||||
}
|
||||
yield MathUtil.negativeToZero(downPaymentMoney.minus(MathUtil.negativeToZero(disbursementTransaction
|
||||
.getAmount(loan.getCurrency()).minus(disbursementTransaction.getOutstandingLoanBalanceMoney(loan.getCurrency())))));
|
||||
}
|
||||
// For Progressive loan: Disbursement transaction portion balances are enough to see whether the overpayment
|
||||
// amount was more than the calculated down-payment amount
|
||||
case PROGRESSIVE ->
|
||||
MathUtil.negativeToZero(downPaymentMoney.minus(disbursementTransaction.getOverPaymentPortion(loan.getCurrency())));
|
||||
};
|
||||
|
||||
if (adjustedDownPaymentMoney.isGreaterThanZero()) {
|
||||
final LoanTransaction downPaymentTransaction = LoanTransaction.downPayment(loan.getOffice(), adjustedDownPaymentMoney, null,
|
||||
disbursedOn, externalId);
|
||||
final LoanEvent event = LoanEvent.LOAN_REPAYMENT_OR_WAIVER;
|
||||
loanDownPaymentTransactionValidator.validateRepaymentTypeAccountStatus(loan, downPaymentTransaction, event);
|
||||
final HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO();
|
||||
loanDownPaymentTransactionValidator.validateRepaymentDateIsOnHoliday(downPaymentTransaction.getTransactionDate(),
|
||||
holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays());
|
||||
loanDownPaymentTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(downPaymentTransaction.getTransactionDate(),
|
||||
holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
|
||||
|
||||
handleRepaymentOrRecoveryOrWaiverTransaction(loan, downPaymentTransaction, loan.getLoanLifecycleStateMachine(), null,
|
||||
scheduleGeneratorDTO);
|
||||
return downPaymentTransaction;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanRefundValidator;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class LoanRefundService {
|
||||
|
||||
private final LoanRefundValidator loanRefundValidator;
|
||||
|
||||
public void makeRefund(final Loan loan, final LoanTransaction loanTransaction,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
|
||||
final List<Long> existingReversedTransactionIds) {
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
|
||||
loanRefundValidator.validateTransferRefund(loan, loanTransaction);
|
||||
|
||||
loanTransaction.updateLoan(loan);
|
||||
|
||||
if (loanTransaction.isNotZero()) {
|
||||
loan.addLoanTransaction(loanTransaction);
|
||||
}
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
loan.doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
|
||||
}
|
||||
|
||||
public LocalDate extractTransactionDate(final Loan loan, final LoanTransaction loanTransaction) {
|
||||
final LocalDate loanTransactionDate = loanTransaction.getTransactionDate();
|
||||
loanRefundValidator.validateTransactionDateAfterDisbursement(loan, loanTransactionDate);
|
||||
return loanTransactionDate;
|
||||
}
|
||||
|
||||
public void makeRefundForActiveLoan(final Loan loan, final LoanTransaction loanTransaction,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
|
||||
final List<Long> existingReversedTransactionIds) {
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
|
||||
handleRefundTransaction(loan, loanTransaction, loanLifecycleStateMachine, null);
|
||||
}
|
||||
|
||||
public void creditBalanceRefund(final Loan loan, final LoanTransaction newCreditBalanceRefundTransaction,
|
||||
final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, final List<Long> existingTransactionIds,
|
||||
final List<Long> existingReversedTransactionIds) {
|
||||
loanRefundValidator.validateCreditBalanceRefund(loan, newCreditBalanceRefundTransaction);
|
||||
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
|
||||
loan.getLoanTransactions().add(newCreditBalanceRefundTransaction);
|
||||
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
|
||||
if (MathUtil.isEmpty(loan.getTotalOverpaid())) {
|
||||
loan.setOverpaidOnDate(null);
|
||||
loan.setClosedOnDate(newCreditBalanceRefundTransaction.getTransactionDate());
|
||||
defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_CREDIT_BALANCE_REFUND, loan);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRefundTransaction(final Loan loan, final LoanTransaction loanTransaction,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction) {
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REFUND, loan);
|
||||
|
||||
loanTransaction.updateLoan(loan);
|
||||
|
||||
loanRefundValidator.validateRefundEligibility(loan, loanTransaction);
|
||||
|
||||
if (loanTransaction.isNotZero()) {
|
||||
loan.addLoanTransaction(loanTransaction);
|
||||
}
|
||||
|
||||
loanRefundValidator.validateRefundTransactionType(loanTransaction);
|
||||
|
||||
final LocalDate loanTransactionDate = extractTransactionDate(loan, loanTransaction);
|
||||
|
||||
loanRefundValidator.validateTransactionDateNotInFuture(loanTransactionDate);
|
||||
loanRefundValidator.validateTransactionAmountThreshold(loan, adjustedTransaction);
|
||||
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessor();
|
||||
|
||||
// If it's a refund
|
||||
if (adjustedTransaction == null) {
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction,
|
||||
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
|
||||
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
|
||||
} else {
|
||||
loan.reprocessTransactions();
|
||||
}
|
||||
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
loan.doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class LoanScheduleService {
|
||||
|
||||
private final LoanChargeService loanChargeService;
|
||||
|
||||
/**
|
||||
* Ability to regenerate the repayment schedule based on the loans current details/state.
|
||||
*/
|
||||
public void regenerateRepaymentSchedule(final Loan loan, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
|
||||
final LoanScheduleModel loanSchedule = loan.regenerateScheduleModel(scheduleGeneratorDTO);
|
||||
if (loanSchedule == null) {
|
||||
return;
|
||||
}
|
||||
loan.updateLoanSchedule(loanSchedule);
|
||||
final Set<LoanCharge> charges = loan.getActiveCharges();
|
||||
for (final LoanCharge loanCharge : charges) {
|
||||
if (!loanCharge.isWaived()) {
|
||||
loanChargeService.recalculateLoanCharge(loan, loanCharge, scheduleGeneratorDTO.getPenaltyWaitPeriod());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void recalculateScheduleFromLastTransaction(final Loan loan, final ScheduleGeneratorDTO generatorDTO) {
|
||||
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO);
|
||||
} else {
|
||||
regenerateRepaymentSchedule(loan, generatorDTO);
|
||||
}
|
||||
loan.reprocessTransactions();
|
||||
}
|
||||
|
||||
public ChangedTransactionDetail recalculateScheduleFromLastTransaction(final Loan loan, final ScheduleGeneratorDTO generatorDTO,
|
||||
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds) {
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
/*
|
||||
* LocalDate recalculateFrom = null; List<LoanTransaction> loanTransactions =
|
||||
* this.retrieveListOfTransactionsPostDisbursementExcludeAccruals(); for (LoanTransaction loanTransaction :
|
||||
* loanTransactions) { if (recalculateFrom == null ||
|
||||
* loanTransaction.getTransactionDate().isAfter(recalculateFrom)) { recalculateFrom =
|
||||
* loanTransaction.getTransactionDate(); } } generatorDTO.setRecalculateFrom(recalculateFrom);
|
||||
*/
|
||||
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO);
|
||||
} else {
|
||||
regenerateRepaymentSchedule(loan, generatorDTO);
|
||||
}
|
||||
return loan.reprocessTransactions();
|
||||
}
|
||||
|
||||
public void regenerateRepaymentScheduleWithInterestRecalculation(final Loan loan, final ScheduleGeneratorDTO generatorDTO) {
|
||||
final LocalDate lastTransactionDate = loan.getLastUserTransactionDate();
|
||||
final LoanScheduleDTO loanSchedule = loan.getRecalculatedSchedule(generatorDTO);
|
||||
if (loanSchedule == null) {
|
||||
return;
|
||||
}
|
||||
// Either the installments got recalculated or the model
|
||||
if (loanSchedule.getInstallments() != null) {
|
||||
loan.updateLoanSchedule(loanSchedule.getInstallments());
|
||||
} else {
|
||||
loan.updateLoanSchedule(loanSchedule.getLoanScheduleModel());
|
||||
}
|
||||
loan.setInterestRecalculatedOn(DateUtils.getBusinessLocalDate());
|
||||
final LocalDate lastRepaymentDate = loan.getLastRepaymentPeriodDueDate(true);
|
||||
final Set<LoanCharge> charges = loan.getActiveCharges();
|
||||
for (final LoanCharge loanCharge : charges) {
|
||||
if (!loanCharge.isDueAtDisbursement()) {
|
||||
loan.updateOverdueScheduleInstallment(loanCharge);
|
||||
if (loanCharge.getDueLocalDate() == null || !DateUtils.isBefore(lastRepaymentDate, loanCharge.getDueLocalDate())) {
|
||||
if ((loanCharge.isInstalmentFee() || !loanCharge.isWaived()) && (loanCharge.getDueLocalDate() == null
|
||||
|| !DateUtils.isAfter(lastTransactionDate, loanCharge.getDueLocalDate()))) {
|
||||
loanChargeService.recalculateLoanCharge(loan, loanCharge, generatorDTO.getPenaltyWaitPeriod());
|
||||
loanCharge.updateWaivedAmount(loan.getCurrency());
|
||||
}
|
||||
} else {
|
||||
loanCharge.setActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loan.processPostDisbursementTransactions();
|
||||
}
|
||||
|
||||
public ChangedTransactionDetail handleRegenerateRepaymentScheduleWithInterestRecalculation(final Loan loan,
|
||||
final ScheduleGeneratorDTO generatorDTO) {
|
||||
regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO);
|
||||
return loan.reprocessTransactions();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user