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();
|
||||
}
|
||||
}
|
||||
+1
@@ -802,6 +802,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
oldChargebackRelations.forEach(relations::remove);
|
||||
relations.add(LoanTransactionRelation.linkToTransaction(originalTransaction, newTransaction, CHARGEBACK));
|
||||
}
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(oldTransaction.getLoan(), oldTransaction, "reversed");
|
||||
oldTransaction.reverse();
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -82,6 +82,7 @@ import org.apache.fineract.portfolio.group.exception.InvalidGroupStateTransition
|
||||
import org.apache.fineract.portfolio.group.serialization.GroupingTypesDataValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService;
|
||||
import org.apache.fineract.portfolio.note.domain.Note;
|
||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
@@ -115,6 +116,7 @@ public class GroupingTypesWritePlatformServiceJpaRepositoryImpl implements Group
|
||||
private final AccountNumberGenerator accountNumberGenerator;
|
||||
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final LoanOfficerService loanOfficerService;
|
||||
|
||||
private CommandProcessingResult createGroupingType(final JsonCommand command, final GroupTypes groupingType, final Long centerId) {
|
||||
try {
|
||||
@@ -517,7 +519,7 @@ public class GroupingTypesWritePlatformServiceJpaRepositoryImpl implements Group
|
||||
if (this.loanRepositoryWrapper.doNonClosedLoanAccountsExistForClient(client.getId())) {
|
||||
for (final Loan loan : this.loanRepositoryWrapper.findLoanByClientId(client.getId())) {
|
||||
if (loan.isDisbursed() && !loan.isClosed()) {
|
||||
loan.reassignLoanOfficer(staff, loanOfficerReassignmentDate);
|
||||
loanOfficerService.reassignLoanOfficer(loan, staff, loanOfficerReassignmentDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -57,6 +57,7 @@ import org.apache.fineract.portfolio.group.service.GroupRolesWritePlatformServic
|
||||
import org.apache.fineract.portfolio.group.service.GroupingTypesWritePlatformService;
|
||||
import org.apache.fineract.portfolio.group.service.GroupingTypesWritePlatformServiceJpaRepositoryImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService;
|
||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
@@ -90,14 +91,14 @@ public class GroupConfiguration {
|
||||
ConfigurationDomainService configurationDomainService, SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper,
|
||||
AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, AccountNumberGenerator accountNumberGenerator,
|
||||
EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService,
|
||||
BusinessEventNotifierService businessEventNotifierService
|
||||
BusinessEventNotifierService businessEventNotifierService, LoanOfficerService loanOfficerService
|
||||
|
||||
) {
|
||||
return new GroupingTypesWritePlatformServiceJpaRepositoryImpl(context, groupRepository, clientRepositoryWrapper,
|
||||
officeRepositoryWrapper, staffRepository, noteRepository, groupLevelRepository, fromApiJsonDeserializer,
|
||||
loanRepositoryWrapper, codeValueRepository, commandProcessingService, calendarInstanceRepository,
|
||||
configurationDomainService, savingsAccountRepositoryWrapper, accountNumberFormatRepository, accountNumberGenerator,
|
||||
entityDatatableChecksWritePlatformService, businessEventNotifierService
|
||||
entityDatatableChecksWritePlatformService, businessEventNotifierService, loanOfficerService
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
+160
-14
@@ -99,11 +99,19 @@ import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
||||
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.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanForeclosureValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundServiceDelegate;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanRefundService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
|
||||
@@ -148,6 +156,14 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
|
||||
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
|
||||
private final InterestRefundServiceDelegate interestRefundServiceDelegate;
|
||||
private final LoanTransactionValidator loanTransactionValidator;
|
||||
private final LoanForeclosureValidator loanForeclosureValidator;
|
||||
private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator;
|
||||
private final LoanChargeService loanChargeService;
|
||||
private final LoanScheduleService loanScheduleService;
|
||||
private final LoanDownPaymentHandlerService loanDownPaymentHandlerService;
|
||||
private final LoanChargeValidator loanChargeValidator;
|
||||
private final LoanRefundService loanRefundService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -235,9 +251,20 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom,
|
||||
holidayDetailDto);
|
||||
|
||||
final ChangedTransactionDetail changedTransactionDetail = loan.makeRepayment(newRepaymentTransaction,
|
||||
defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, isRecoveryRepayment,
|
||||
scheduleGeneratorDTO, isHolidayValidationDone);
|
||||
if (!isHolidayValidationDone) {
|
||||
final HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO();
|
||||
loanTransactionValidator.validateRepaymentDateIsOnHoliday(newRepaymentTransaction.getTransactionDate(),
|
||||
holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays());
|
||||
loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newRepaymentTransaction.getTransactionDate(),
|
||||
holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
|
||||
}
|
||||
final LoanEvent event = isRecoveryRepayment ? LoanEvent.LOAN_RECOVERY_PAYMENT : LoanEvent.LOAN_REPAYMENT_OR_WAIVER;
|
||||
loanTransactionValidator.validateActivityNotBeforeLastTransactionDate(loan, newRepaymentTransaction.getTransactionDate(), event);
|
||||
loanDownPaymentTransactionValidator.validateRepaymentTypeAccountStatus(loan, newRepaymentTransaction, event);
|
||||
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, event,
|
||||
newRepaymentTransaction.getTransactionDate());
|
||||
final ChangedTransactionDetail changedTransactionDetail = makeRepayment(loan, newRepaymentTransaction,
|
||||
defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO);
|
||||
|
||||
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
@@ -445,8 +472,17 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday,
|
||||
allowTransactionsOnNonWorkingDay);
|
||||
|
||||
loan.makeChargePayment(chargeId, defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds,
|
||||
holidayDetailDTO, newPaymentTransaction, installmentNumber);
|
||||
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_CHARGE_PAYMENT);
|
||||
loanTransactionValidator.validateRepaymentDateIsOnHoliday(newPaymentTransaction.getTransactionDate(),
|
||||
holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays());
|
||||
loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newPaymentTransaction.getTransactionDate(),
|
||||
holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
|
||||
loanTransactionValidator.validateActivityNotBeforeLastTransactionDate(loan, newPaymentTransaction.getTransactionDate(),
|
||||
LoanEvent.LOAN_CHARGE_PAYMENT);
|
||||
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_CHARGE_PAYMENT,
|
||||
newPaymentTransaction.getTransactionDate());
|
||||
loanChargeService.makeChargePayment(loan, chargeId, defaultLoanLifecycleStateMachine, existingTransactionIds,
|
||||
existingReversedTransactionIds, newPaymentTransaction, installmentNumber);
|
||||
}
|
||||
saveLoanTransactionWithDataIntegrityViolationChecks(newPaymentTransaction);
|
||||
saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
||||
@@ -532,8 +568,13 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
final WorkingDays workingDays = this.workingDaysRepository.findOne();
|
||||
final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled();
|
||||
|
||||
loan.makeRefund(newRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds,
|
||||
allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay);
|
||||
loanTransactionValidator.validateRepaymentDateIsOnHoliday(newRefundTransaction.getTransactionDate(), allowTransactionsOnHoliday,
|
||||
holidays);
|
||||
loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newRefundTransaction.getTransactionDate(), workingDays,
|
||||
allowTransactionsOnNonWorkingDay);
|
||||
|
||||
loanRefundService.makeRefund(loan, newRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds,
|
||||
existingReversedTransactionIds);
|
||||
|
||||
saveLoanTransactionWithDataIntegrityViolationChecks(newRefundTransaction);
|
||||
this.loanRepositoryWrapper.saveAndFlush(loan);
|
||||
@@ -605,6 +646,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
+ " reversal is not allowed before or on the date when the loan got charged-off",
|
||||
loanTransaction.getId());
|
||||
}
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, "reversed");
|
||||
loanTransaction.reverse();
|
||||
saveLoanTransactionWithDataIntegrityViolationChecks(loanTransaction);
|
||||
}
|
||||
@@ -671,8 +713,11 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
LoanTransaction newCreditBalanceRefundTransaction = LoanTransaction.creditBalanceRefund(loan, loan.getOffice(), refundAmount,
|
||||
transactionDate, externalId, paymentDetail);
|
||||
|
||||
loan.creditBalanceRefund(newCreditBalanceRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds,
|
||||
existingReversedTransactionIds);
|
||||
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_CREDIT_BALANCE_REFUND);
|
||||
loanTransactionValidator.validateRefundDateIsAfterLastRepayment(loan, newCreditBalanceRefundTransaction.getTransactionDate());
|
||||
|
||||
loanRefundService.creditBalanceRefund(loan, newCreditBalanceRefundTransaction, defaultLoanLifecycleStateMachine,
|
||||
existingTransactionIds, existingReversedTransactionIds);
|
||||
|
||||
newCreditBalanceRefundTransaction = this.loanTransactionRepository.saveAndFlush(newCreditBalanceRefundTransaction);
|
||||
|
||||
@@ -709,14 +754,22 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
}
|
||||
final LoanTransaction newRefundTransaction = LoanTransaction.refundForActiveLoan(loan.getOffice(), refundAmount, paymentDetail,
|
||||
transactionDate, txnExternalId);
|
||||
loanTransactionValidator.validateRefundDateIsAfterLastRepayment(loan, newRefundTransaction.getTransactionDate());
|
||||
final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled();
|
||||
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), transactionDate,
|
||||
HolidayStatusType.ACTIVE.getValue());
|
||||
final WorkingDays workingDays = this.workingDaysRepository.findOne();
|
||||
final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled();
|
||||
|
||||
loan.makeRefundForActiveLoan(newRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds,
|
||||
existingReversedTransactionIds, allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay);
|
||||
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_REFUND);
|
||||
loanTransactionValidator.validateRepaymentDateIsOnHoliday(newRefundTransaction.getTransactionDate(), allowTransactionsOnHoliday,
|
||||
holidays);
|
||||
loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newRefundTransaction.getTransactionDate(), workingDays,
|
||||
allowTransactionsOnNonWorkingDay);
|
||||
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_REFUND,
|
||||
newRefundTransaction.getTransactionDate());
|
||||
loanRefundService.makeRefundForActiveLoan(loan, newRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds,
|
||||
existingReversedTransactionIds);
|
||||
|
||||
this.loanTransactionRepository.saveAndFlush(newRefundTransaction);
|
||||
|
||||
@@ -765,7 +818,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
Money feePayable = foreCloseDetail.getFeeChargesCharged(currency);
|
||||
Money penaltyPayable = foreCloseDetail.getPenaltyChargesCharged(currency);
|
||||
Money payPrincipal = foreCloseDetail.getPrincipal(currency);
|
||||
loan.updateInstallmentsPostDate(foreClosureDate);
|
||||
updateInstallmentsPostDate(loan, foreClosureDate);
|
||||
|
||||
LoanTransaction payment = null;
|
||||
|
||||
@@ -778,7 +831,11 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
}
|
||||
|
||||
List<Long> transactionIds = new ArrayList<>();
|
||||
final ChangedTransactionDetail changedTransactionDetail = loan.handleForeClosureTransactions(payment,
|
||||
if (payment != null) {
|
||||
loanForeclosureValidator.validateForForeclosure(loan, payment.getTransactionDate());
|
||||
}
|
||||
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_FORECLOSURE);
|
||||
final ChangedTransactionDetail changedTransactionDetail = handleForeClosureTransactions(loan, payment,
|
||||
defaultLoanLifecycleStateMachine, scheduleGeneratorDTO);
|
||||
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
@@ -916,7 +973,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
}
|
||||
} else {
|
||||
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
|
||||
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
|
||||
}
|
||||
loan.getLoanTransactions().add(refundTransaction);
|
||||
if (interestRefundTransaction != null) {
|
||||
@@ -1015,4 +1072,93 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
|
||||
return interestRefundTransaction;
|
||||
}
|
||||
|
||||
private void updateInstallmentsPostDate(final Loan loan, final LocalDate transactionDate) {
|
||||
final List<LoanRepaymentScheduleInstallment> newInstallments = new ArrayList<>(loan.getRepaymentScheduleInstallments());
|
||||
final MonetaryCurrency currency = loan.getCurrency();
|
||||
Money totalPrincipal = Money.zero(currency);
|
||||
final Money[] balances = loan.retriveIncomeForOverlappingPeriod(transactionDate);
|
||||
boolean isInterestComponent = true;
|
||||
for (final LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
|
||||
if (!DateUtils.isAfter(transactionDate, installment.getDueDate())) {
|
||||
totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency));
|
||||
newInstallments.remove(installment);
|
||||
if (DateUtils.isEqual(transactionDate, installment.getDueDate())) {
|
||||
isInterestComponent = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (LoanDisbursementDetails loanDisbursementDetails : loan.getDisbursementDetails()) {
|
||||
if (loanDisbursementDetails.actualDisbursementDate() == null) {
|
||||
totalPrincipal = Money.of(currency, totalPrincipal.getAmount().subtract(loanDisbursementDetails.principal()));
|
||||
}
|
||||
}
|
||||
|
||||
LocalDate installmentStartDate = loan.getDisbursementDate();
|
||||
|
||||
if (!newInstallments.isEmpty()) {
|
||||
installmentStartDate = newInstallments.get(newInstallments.size() - 1).getDueDate();
|
||||
}
|
||||
|
||||
int installmentNumber = newInstallments.size();
|
||||
|
||||
if (!isInterestComponent) {
|
||||
installmentNumber++;
|
||||
}
|
||||
|
||||
final LoanRepaymentScheduleInstallment newInstallment = new LoanRepaymentScheduleInstallment(null, newInstallments.size() + 1,
|
||||
installmentStartDate, transactionDate, totalPrincipal.getAmount(), balances[0].getAmount(), balances[1].getAmount(),
|
||||
balances[2].getAmount(), isInterestComponent, null);
|
||||
newInstallment.updateInstallmentNumber(newInstallments.size() + 1);
|
||||
newInstallments.add(newInstallment);
|
||||
loan.updateLoanScheduleOnForeclosure(newInstallments);
|
||||
|
||||
final Set<LoanCharge> charges = loan.getActiveCharges();
|
||||
final int penaltyWaitPeriod = 0;
|
||||
for (LoanCharge loanCharge : charges) {
|
||||
if (DateUtils.isAfter(loanCharge.getDueLocalDate(), transactionDate)) {
|
||||
loanCharge.setActive(false);
|
||||
} else if (loanCharge.getDueLocalDate() == null) {
|
||||
loanChargeService.recalculateLoanCharge(loan, loanCharge, penaltyWaitPeriod);
|
||||
loanCharge.updateWaivedAmount(currency);
|
||||
}
|
||||
}
|
||||
|
||||
for (LoanTransaction loanTransaction : loan.getLoanTransactions()) {
|
||||
if (loanTransaction.isChargesWaiver()) {
|
||||
for (LoanChargePaidBy chargePaidBy : loanTransaction.getLoanChargesPaid()) {
|
||||
if ((chargePaidBy.getLoanCharge().isDueDateCharge()
|
||||
&& DateUtils.isBefore(transactionDate, chargePaidBy.getLoanCharge().getDueLocalDate()))
|
||||
|| (chargePaidBy.getLoanCharge().isInstalmentFee() && chargePaidBy.getInstallmentNumber() != null
|
||||
&& chargePaidBy.getInstallmentNumber() > installmentNumber)) {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(),
|
||||
loanTransaction, "reversed");
|
||||
loanTransaction.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
private ChangedTransactionDetail makeRepayment(final Loan loan, final LoanTransaction repaymentTransaction,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
|
||||
final List<Long> existingReversedTransactionIds, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loan, repaymentTransaction, "created");
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
|
||||
return loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, repaymentTransaction,
|
||||
loanLifecycleStateMachine, null, scheduleGeneratorDTO);
|
||||
}
|
||||
|
||||
private ChangedTransactionDetail handleForeClosureTransactions(final Loan loan, final LoanTransaction repaymentTransaction,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
|
||||
loan.setLoanSubStatus(LoanSubStatus.FORECLOSED.getValue());
|
||||
return loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, repaymentTransaction,
|
||||
loanLifecycleStateMachine, null, scheduleGeneratorDTO);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-4
@@ -115,7 +115,10 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.VariableLoanScheduleFromApiJsonValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementDetailsAssembler;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
|
||||
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
|
||||
@@ -158,6 +161,9 @@ public class LoanScheduleAssembler {
|
||||
private final LoanRepositoryWrapper loanRepositoryWrapper;
|
||||
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
|
||||
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
|
||||
private final LoanDisbursementService loanDisbursementService;
|
||||
private final LoanChargeService loanChargeService;
|
||||
private final LoanScheduleService loanScheduleService;
|
||||
|
||||
public LoanApplicationTerms assembleLoanTerms(final JsonElement element) {
|
||||
final Long loanProductId = this.fromApiJsonHelper.extractLongNamed("productId", element);
|
||||
@@ -936,7 +942,7 @@ public class LoanScheduleAssembler {
|
||||
}
|
||||
final LocalDate recalculateFrom = null;
|
||||
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
|
||||
loan.regenerateRepaymentSchedule(scheduleGeneratorDTO);
|
||||
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
|
||||
}
|
||||
@@ -1478,11 +1484,11 @@ public class LoanScheduleAssembler {
|
||||
actualChanges.put(LoanApiConstants.disbursementNetDisbursalAmountParameterName, loan.getNetDisbursalAmount());
|
||||
|
||||
if (disbursementDataArray != null) {
|
||||
loan.updateDisbursementDetails(command, actualChanges);
|
||||
loanDisbursementService.updateDisbursementDetails(loan, command, actualChanges);
|
||||
}
|
||||
}
|
||||
|
||||
loan.recalculateAllCharges();
|
||||
loanChargeService.recalculateAllCharges(loan);
|
||||
|
||||
loan.setApprovedOnDate(approvedOn);
|
||||
loan.setApprovedBy(currentUser);
|
||||
@@ -1507,7 +1513,7 @@ public class LoanScheduleAssembler {
|
||||
if (!actualChanges.isEmpty()) {
|
||||
if (actualChanges.containsKey(LoanApiConstants.approvedLoanAmountParameterName)
|
||||
|| actualChanges.containsKey("recalculateLoanSchedule") || actualChanges.containsKey("expectedDisbursementDate")) {
|
||||
loan.regenerateRepaymentSchedule(loanUtilService.buildScheduleGeneratorDTO(loan, null));
|
||||
loanScheduleService.regenerateRepaymentSchedule(loan, loanUtilService.buildScheduleGeneratorDTO(loan, null));
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -37,6 +37,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -52,6 +53,7 @@ public class LoanScheduleWritePlatformServiceImpl implements LoanScheduleWritePl
|
||||
private final LoanUtilService loanUtilService;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
|
||||
private final LoanScheduleService loanScheduleService;
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult addLoanScheduleVariations(final Long loanId, final JsonCommand command) {
|
||||
@@ -98,7 +100,7 @@ public class LoanScheduleWritePlatformServiceImpl implements LoanScheduleWritePl
|
||||
loan.getLoanTermVariations().clear();
|
||||
final LocalDate recalculateFrom = null;
|
||||
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
|
||||
loan.regenerateRepaymentSchedule(scheduleGeneratorDTO);
|
||||
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
loanAccountDomainService.saveLoanWithDataIntegrityViolationChecks(loan);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanScheduleVariationsDeletedBusinessEvent(loan));
|
||||
|
||||
+3
-1
@@ -81,6 +81,7 @@ import org.apache.fineract.portfolio.loanaccount.rescheduleloan.exception.LoanRe
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
@@ -121,6 +122,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
|
||||
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
|
||||
private final LoanChargeService loanChargeService;
|
||||
|
||||
/**
|
||||
* create a new instance of the LoanRescheduleRequest object from the JsonCommand object and persist
|
||||
@@ -439,7 +441,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
|
||||
loan.updateLoanSchedule(loanSchedule.getLoanScheduleModel());
|
||||
}
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
loan.recalculateAllCharges();
|
||||
loanChargeService.recalculateAllCharges(loan);
|
||||
ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions();
|
||||
|
||||
this.loanRepaymentScheduleHistoryRepository.saveAll(loanRepaymentScheduleHistoryList);
|
||||
|
||||
+1
-1
@@ -2106,7 +2106,7 @@ public final class LoanApplicationValidator {
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal getOverAppliedMax(Loan loan) {
|
||||
public BigDecimal getOverAppliedMax(Loan loan) {
|
||||
LoanProduct loanProduct = loan.getLoanProduct();
|
||||
if ("percentage".equals(loanProduct.getOverAppliedCalculationType())) {
|
||||
BigDecimal overAppliedNumber = BigDecimal.valueOf(loanProduct.getOverAppliedNumber());
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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 lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanDisbursalException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public final class LoanDisbursementValidator {
|
||||
|
||||
private final LoanApplicationValidator loanApplicationValidator;
|
||||
|
||||
public void compareDisbursedToApprovedOrProposedPrincipal(final Loan loan, final BigDecimal disbursedAmount,
|
||||
final BigDecimal totalDisbursed) {
|
||||
if (loan.loanProduct().isDisallowExpectedDisbursements() && loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) {
|
||||
final BigDecimal maxDisbursedAmount = loanApplicationValidator.getOverAppliedMax(loan);
|
||||
if (totalDisbursed.compareTo(maxDisbursedAmount) > 0) {
|
||||
final String errorMessage = String.format(
|
||||
"Loan disbursal amount can't be greater than maximum applied loan amount calculation. "
|
||||
+ "Total disbursed amount: %s Maximum disbursal amount: %s",
|
||||
totalDisbursed.stripTrailingZeros().toPlainString(), maxDisbursedAmount.stripTrailingZeros().toPlainString());
|
||||
throw new InvalidLoanStateTransitionException("disbursal",
|
||||
"amount.can't.be.greater.than.maximum.applied.loan.amount.calculation", errorMessage, disbursedAmount,
|
||||
maxDisbursedAmount);
|
||||
}
|
||||
} else {
|
||||
if (totalDisbursed.compareTo(loan.getApprovedPrincipal()) > 0) {
|
||||
final String errorMsg = "Loan can't be disbursed,disburse amount is exceeding approved principal ";
|
||||
throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.principal", totalDisbursed,
|
||||
loan.getApprovedPrincipal());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void validateDisburseAmountNotExceedingApprovedAmount(final Loan loan, final BigDecimal diff,
|
||||
final BigDecimal principalDisbursed) {
|
||||
if (!loan.loanProduct().isMultiDisburseLoan() && diff.compareTo(BigDecimal.ZERO) < 0) {
|
||||
final String errorMsg = "Loan can't be disbursed,disburse amount is exceeding approved amount ";
|
||||
throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.amount", principalDisbursed,
|
||||
loan.getLoanRepaymentScheduleDetail().getPrincipal().getAmount());
|
||||
}
|
||||
}
|
||||
|
||||
public void validateDisburseDate(final Loan loan, final LocalDate disbursedOn, final LocalDate expectedDate) {
|
||||
if (expectedDate != null
|
||||
&& (DateUtils.isAfter(disbursedOn, loan.fetchRepaymentScheduleInstallment(1).getDueDate())
|
||||
|| DateUtils.isAfter(disbursedOn, expectedDate))
|
||||
&& DateUtils.isEqual(disbursedOn, loan.getActualDisbursementDate())) {
|
||||
final String errorMessage = "submittedOnDate cannot be after the loans expectedFirstRepaymentOnDate: " + expectedDate;
|
||||
throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.after.expected.first.repayment.date", errorMessage,
|
||||
disbursedOn, expectedDate);
|
||||
}
|
||||
|
||||
if (DateUtils.isDateInTheFuture(disbursedOn)) {
|
||||
final String errorMessage = "The date on which a loan with identifier : " + loan.getAccountNumber()
|
||||
+ " is disbursed cannot be in the future.";
|
||||
throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.a.future.date", errorMessage, disbursedOn);
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public final class LoanForeclosureValidator {
|
||||
|
||||
public void validateForForeclosure(final Loan loan, final LocalDate transactionDate) {
|
||||
if (loan.isInterestRecalculationEnabledForProduct()) {
|
||||
final String defaultUserMessage = "The loan with interest recalculation enabled cannot be foreclosed.";
|
||||
throw new LoanForeclosureException("loan.with.interest.recalculation.enabled.cannot.be.foreclosured", defaultUserMessage,
|
||||
loan.getId());
|
||||
}
|
||||
|
||||
if (DateUtils.isDateInTheFuture(transactionDate)) {
|
||||
final String defaultUserMessage = "The transactionDate cannot be in the future.";
|
||||
throw new LoanForeclosureException("loan.foreclosure.transaction.date.is.in.future", defaultUserMessage, transactionDate);
|
||||
}
|
||||
|
||||
if (DateUtils.isBefore(transactionDate, loan.getLastUserTransactionDate())) {
|
||||
final String defaultUserMessage = "The transactionDate cannot be earlier than the last transaction date.";
|
||||
throw new LoanForeclosureException("loan.foreclosure.transaction.date.cannot.before.the.last.transaction.date",
|
||||
defaultUserMessage, transactionDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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.Optional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanOfficerAssignmentHistory;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerUnassignmentDateException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public final class LoanOfficerValidator {
|
||||
|
||||
public void validateUnassignDate(final Loan loan, final LocalDate unassignDate) {
|
||||
final Optional<LoanOfficerAssignmentHistory> latestHistoryRecordOptional = loan.findLatestIncompleteHistoryRecord();
|
||||
|
||||
if (latestHistoryRecordOptional.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final LocalDate startDate = latestHistoryRecordOptional.get().getStartDate();
|
||||
if (DateUtils.isAfter(startDate, unassignDate)) {
|
||||
final String errorMessage = "The Loan officer Unassign date(" + unassignDate + ") cannot be before its assignment date ("
|
||||
+ startDate + ").";
|
||||
throw new LoanOfficerUnassignmentDateException("cannot.be.before.assignment.date", errorMessage, loan.getId(),
|
||||
loan.getLoanOfficer().getId(), startDate, unassignDate);
|
||||
} else if (DateUtils.isDateInTheFuture(unassignDate)) {
|
||||
final String errorMessage = "The Loan Officer Unassign date (" + unassignDate + ") cannot be in the future.";
|
||||
throw new LoanOfficerUnassignmentDateException("cannot.be.a.future.date", errorMessage, unassignDate);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateReassignment(final Loan loan, final LocalDate assignmentDate,
|
||||
final LoanOfficerAssignmentHistory lastAssignmentRecord) {
|
||||
if (loan.isSubmittedOnDateAfter(assignmentDate)) {
|
||||
final String errorMessage = "The Loan Officer assignment date (" + assignmentDate.toString()
|
||||
+ ") cannot be before loan submitted date (" + loan.getSubmittedOnDate().toString() + ").";
|
||||
throw new LoanOfficerAssignmentDateException("cannot.be.before.loan.submittal.date", errorMessage, assignmentDate,
|
||||
loan.getSubmittedOnDate());
|
||||
}
|
||||
|
||||
if (lastAssignmentRecord != null && lastAssignmentRecord.isEndDateAfter(assignmentDate)) {
|
||||
final String errorMessage = "The Loan Officer assignment date (" + assignmentDate
|
||||
+ ") cannot be before previous Loan Officer unassigned date (" + lastAssignmentRecord.getEndDate() + ").";
|
||||
throw new LoanOfficerAssignmentDateException("cannot.be.before.previous.unassignement.date", errorMessage, assignmentDate,
|
||||
lastAssignmentRecord.getEndDate());
|
||||
}
|
||||
|
||||
if (DateUtils.isDateInTheFuture(assignmentDate)) {
|
||||
final String errorMessage = "The Loan Officer assignment date (" + assignmentDate + ") cannot be in the future.";
|
||||
throw new LoanOfficerAssignmentDateException("cannot.be.a.future.date", errorMessage, assignmentDate);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateAssignmentDateWithHistory(final Loan loan, final Optional<LoanOfficerAssignmentHistory> latestHistoryRecord,
|
||||
final LocalDate assignmentDate) {
|
||||
if (latestHistoryRecord.isPresent() && latestHistoryRecord.get().isBeforeStartDate(assignmentDate)) {
|
||||
final String errorMessage = "Loan with identifier " + loan.getId() + " was already assigned before date " + assignmentDate;
|
||||
throw new LoanOfficerAssignmentDateException("is.before.last.assignment.date", errorMessage, loan.getId(), assignmentDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
+186
-98
@@ -79,6 +79,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidRefundDateException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanDisbursalException;
|
||||
@@ -102,6 +104,7 @@ public final class LoanTransactionValidator {
|
||||
private final LoanUtilService loanUtilService;
|
||||
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
|
||||
private final CalendarInstanceRepository calendarInstanceRepository;
|
||||
private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator;
|
||||
|
||||
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
@@ -233,34 +236,6 @@ public final class LoanTransactionValidator {
|
||||
});
|
||||
}
|
||||
|
||||
private static @NotNull BigDecimal collectTotalCollateral(Set<LoanCollateralManagement> loanCollateralManagements) {
|
||||
BigDecimal totalCollateral = BigDecimal.ZERO;
|
||||
|
||||
for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) {
|
||||
BigDecimal quantity = loanCollateralManagement.getQuantity();
|
||||
BigDecimal pctToBase = loanCollateralManagement.getClientCollateralManagement().getCollaterals().getPctToBase();
|
||||
BigDecimal basePrice = loanCollateralManagement.getClientCollateralManagement().getCollaterals().getBasePrice();
|
||||
totalCollateral = totalCollateral.add(quantity.multiply(basePrice).multiply(pctToBase).divide(BigDecimal.valueOf(100)));
|
||||
}
|
||||
return totalCollateral;
|
||||
}
|
||||
|
||||
private static @NotNull Set<String> getDisbursementParameters(boolean isAccountTransfer) {
|
||||
Set<String> disbursementParameters;
|
||||
|
||||
if (isAccountTransfer) {
|
||||
disbursementParameters = new HashSet<>(Arrays.asList("actualDisbursementDate", "externalId", "note", "locale", "dateFormat",
|
||||
LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.fixedEmiAmountParameterName,
|
||||
LoanApiConstants.disbursementNetDisbursalAmountParameterName));
|
||||
} else {
|
||||
disbursementParameters = new HashSet<>(Arrays.asList("actualDisbursementDate", "externalId", "note", "locale", "dateFormat",
|
||||
"paymentTypeId", "accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber", "adjustRepaymentDate",
|
||||
LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.fixedEmiAmountParameterName,
|
||||
LoanApiConstants.postDatedChecks, LoanApiConstants.disbursementNetDisbursalAmountParameterName));
|
||||
}
|
||||
return disbursementParameters;
|
||||
}
|
||||
|
||||
public void validateDisbursementWithPostDatedChecks(final String json, final Long loanId) {
|
||||
final JsonElement jsonElement = this.fromApiJsonHelper.parse(json);
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
@@ -411,50 +386,6 @@ public final class LoanTransactionValidator {
|
||||
validatePaymentTransaction(json);
|
||||
}
|
||||
|
||||
private void validatePaymentTransaction(String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
|
||||
final Set<String> transactionParameters = new HashSet<>(
|
||||
Arrays.asList("transactionDate", "transactionAmount", "externalId", "note", "locale", "dateFormat", "paymentTypeId",
|
||||
"accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber", "loanId"));
|
||||
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters);
|
||||
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
|
||||
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element);
|
||||
baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull();
|
||||
|
||||
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
|
||||
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount();
|
||||
|
||||
final String note = this.fromApiJsonHelper.extractStringNamed("note", element);
|
||||
baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000);
|
||||
|
||||
validatePaymentDetails(baseDataValidator, element);
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
private void validatePaymentDetails(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
|
||||
// Validate all string payment detail fields for max length
|
||||
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("paymentTypeId", element);
|
||||
|
||||
baseDataValidator.reset().parameter("paymentTypeId").value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
|
||||
|
||||
final Set<String> paymentDetailParameters = new HashSet<>(
|
||||
Arrays.asList("accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber"));
|
||||
for (final String paymentDetailParameterName : paymentDetailParameters) {
|
||||
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
|
||||
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
|
||||
.notExceedingLengthOf(50);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateTransactionWithNoAmount(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
@@ -705,17 +636,6 @@ public final class LoanTransactionValidator {
|
||||
}
|
||||
}
|
||||
|
||||
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 validateLoanHasNoLaterChargeRefundTransactionToReverseOrCreateATransaction(Loan loan, LocalDate transactionDate,
|
||||
String reversedOrCreated) {
|
||||
for (LoanTransaction txn : loan.getLoanTransactions()) {
|
||||
@@ -834,7 +754,7 @@ public final class LoanTransactionValidator {
|
||||
validateLoanClientIsActive(loan);
|
||||
validateLoanHasCurrency(loan);
|
||||
validateLoanGroupIsActive(loan);
|
||||
validateLoanStatusIsActiveOrFullyPaidOrOverpaid(loan);
|
||||
loanDownPaymentTransactionValidator.validateLoanStatusIsActiveOrFullyPaidOrOverpaid(loan);
|
||||
validateLoanDisbursementIsBeforeTransactionDate(loan, transactionDate);
|
||||
validateLoanHasNoLaterChargeRefundTransactionToReverseOrCreateATransaction(loan, transactionDate, "created");
|
||||
|
||||
@@ -868,7 +788,7 @@ public final class LoanTransactionValidator {
|
||||
public void validateRefund(final Loan loan, LoanTransactionType loanTransactionType, final LocalDate transactionDate,
|
||||
ScheduleGeneratorDTO scheduleGeneratorDTO) {
|
||||
checkClientOrGroupActive(loan);
|
||||
validateLoanStatusIsActiveOrFullyPaidOrOverpaid(loan);
|
||||
loanDownPaymentTransactionValidator.validateLoanStatusIsActiveOrFullyPaidOrOverpaid(loan);
|
||||
validateActivityNotBeforeClientOrGroupTransferDate(loan, transactionDate);
|
||||
validateRepaymentTypeTransactionNotBeforeAChargeRefund(loan, loanTransactionType, transactionDate);
|
||||
validateTransactionNotBeforeLastTransactionDate(loan, loanTransactionType, transactionDate);
|
||||
@@ -880,6 +800,165 @@ public final class LoanTransactionValidator {
|
||||
validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(loan);
|
||||
}
|
||||
|
||||
public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final Loan loan, final LoanTransactionType loanTransactionType,
|
||||
final LocalDate transactionDate) {
|
||||
if (loanTransactionType.isRepaymentType() && !loanTransactionType.isChargeRefund()) {
|
||||
for (LoanTransaction txn : loan.getLoanTransactions()) {
|
||||
if (txn.isChargeRefund() && DateUtils.isBefore(transactionDate, txn.getTransactionDate())) {
|
||||
final String errorMessage = "loan.transaction.cant.be.created.because.later.charge.refund.exists";
|
||||
final String details = "Loan Transaction: " + loan.getId() + " Can't be created because a Later Charge Refund Exists.";
|
||||
throw new LoanChargeRefundException(errorMessage, details);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void validateRefundDateIsAfterLastRepayment(final Loan loan, final LocalDate refundTransactionDate) {
|
||||
final LocalDate possibleNextRefundDate = loan.possibleNextRefundDate();
|
||||
|
||||
if (possibleNextRefundDate == null || DateUtils.isBefore(refundTransactionDate, possibleNextRefundDate)) {
|
||||
throw new InvalidRefundDateException(refundTransactionDate.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public void validateActivityNotBeforeClientOrGroupTransferDate(final Loan loan, final LoanEvent event, final LocalDate activityDate) {
|
||||
if (loan.getClient() != null && loan.getClient().getOfficeJoiningDate() != null) {
|
||||
final LocalDate clientOfficeJoiningDate = loan.getClient().getOfficeJoiningDate();
|
||||
if (DateUtils.isBefore(activityDate, clientOfficeJoiningDate)) {
|
||||
String errorMessage = null;
|
||||
String action = null;
|
||||
String postfix = null;
|
||||
switch (event) {
|
||||
case LOAN_APPROVED -> {
|
||||
errorMessage = "The date on which a loan is approved cannot be earlier than client's transfer date to this office";
|
||||
action = "approval";
|
||||
postfix = "cannot.be.before.client.transfer.date";
|
||||
}
|
||||
case LOAN_APPROVAL_UNDO -> {
|
||||
errorMessage = "The date on which a loan is approved cannot be earlier than client's transfer date to this office";
|
||||
action = "approval";
|
||||
postfix = "cannot.be.undone.before.client.transfer.date";
|
||||
}
|
||||
case LOAN_DISBURSED -> {
|
||||
errorMessage = "The date on which a loan is disbursed cannot be earlier than client's transfer date to this office";
|
||||
action = "disbursal";
|
||||
postfix = "cannot.be.before.client.transfer.date";
|
||||
}
|
||||
case LOAN_DISBURSAL_UNDO -> {
|
||||
errorMessage = "Cannot undo a disbursal done in another branch";
|
||||
action = "disbursal";
|
||||
postfix = "cannot.be.undone.before.client.transfer.date";
|
||||
}
|
||||
case LOAN_REPAYMENT_OR_WAIVER -> {
|
||||
errorMessage = "The date on which a repayment or waiver is made cannot be earlier than client's transfer date to this office";
|
||||
action = "repayment.or.waiver";
|
||||
postfix = "cannot.be.made.before.client.transfer.date";
|
||||
}
|
||||
case WRITE_OFF_OUTSTANDING -> {
|
||||
errorMessage = "The date on which a write off is made cannot be earlier than client's transfer date to this office";
|
||||
action = "writeoff";
|
||||
postfix = "cannot.be.undone.before.client.transfer.date";
|
||||
}
|
||||
case REPAID_IN_FULL -> {
|
||||
errorMessage = "The date on which the loan is repaid in full cannot be earlier than client's transfer date to this office";
|
||||
action = "close";
|
||||
postfix = "cannot.be.undone.before.client.transfer.date";
|
||||
}
|
||||
case LOAN_CHARGE_PAYMENT -> {
|
||||
errorMessage = "The date on which a charge payment is made cannot be earlier than client's transfer date to this office";
|
||||
action = "charge.payment";
|
||||
postfix = "cannot.be.made.before.client.transfer.date";
|
||||
}
|
||||
case LOAN_REFUND -> {
|
||||
errorMessage = "The date on which a refund is made cannot be earlier than client's transfer date to this office";
|
||||
action = "refund";
|
||||
postfix = "cannot.be.made.before.client.transfer.date";
|
||||
}
|
||||
case LOAN_DISBURSAL_UNDO_LAST -> {
|
||||
errorMessage = "Cannot undo a last disbursal in another branch";
|
||||
action = "disbursal";
|
||||
postfix = "cannot.be.undone.before.client.transfer.date";
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, clientOfficeJoiningDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static @NotNull BigDecimal collectTotalCollateral(Set<LoanCollateralManagement> loanCollateralManagements) {
|
||||
BigDecimal totalCollateral = BigDecimal.ZERO;
|
||||
|
||||
for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) {
|
||||
BigDecimal quantity = loanCollateralManagement.getQuantity();
|
||||
BigDecimal pctToBase = loanCollateralManagement.getClientCollateralManagement().getCollaterals().getPctToBase();
|
||||
BigDecimal basePrice = loanCollateralManagement.getClientCollateralManagement().getCollaterals().getBasePrice();
|
||||
totalCollateral = totalCollateral.add(quantity.multiply(basePrice).multiply(pctToBase).divide(BigDecimal.valueOf(100)));
|
||||
}
|
||||
return totalCollateral;
|
||||
}
|
||||
|
||||
private static @NotNull Set<String> getDisbursementParameters(boolean isAccountTransfer) {
|
||||
Set<String> disbursementParameters;
|
||||
|
||||
if (isAccountTransfer) {
|
||||
disbursementParameters = new HashSet<>(Arrays.asList("actualDisbursementDate", "externalId", "note", "locale", "dateFormat",
|
||||
LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.fixedEmiAmountParameterName,
|
||||
LoanApiConstants.disbursementNetDisbursalAmountParameterName));
|
||||
} else {
|
||||
disbursementParameters = new HashSet<>(Arrays.asList("actualDisbursementDate", "externalId", "note", "locale", "dateFormat",
|
||||
"paymentTypeId", "accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber", "adjustRepaymentDate",
|
||||
LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.fixedEmiAmountParameterName,
|
||||
LoanApiConstants.postDatedChecks, LoanApiConstants.disbursementNetDisbursalAmountParameterName));
|
||||
}
|
||||
return disbursementParameters;
|
||||
}
|
||||
|
||||
private void validatePaymentTransaction(String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
|
||||
final Set<String> transactionParameters = new HashSet<>(
|
||||
Arrays.asList("transactionDate", "transactionAmount", "externalId", "note", "locale", "dateFormat", "paymentTypeId",
|
||||
"accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber", "loanId"));
|
||||
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters);
|
||||
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
|
||||
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element);
|
||||
baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull();
|
||||
|
||||
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
|
||||
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount();
|
||||
|
||||
final String note = this.fromApiJsonHelper.extractStringNamed("note", element);
|
||||
baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000);
|
||||
|
||||
validatePaymentDetails(baseDataValidator, element);
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
private void validatePaymentDetails(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
|
||||
// Validate all string payment detail fields for max length
|
||||
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("paymentTypeId", element);
|
||||
|
||||
baseDataValidator.reset().parameter("paymentTypeId").value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
|
||||
|
||||
final Set<String> paymentDetailParameters = new HashSet<>(
|
||||
Arrays.asList("accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber"));
|
||||
for (final String paymentDetailParameterName : paymentDetailParameters) {
|
||||
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
|
||||
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
|
||||
.notExceedingLengthOf(50);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkClientOrGroupActive(final Loan loan) {
|
||||
final Client client = loan.client();
|
||||
if (client != null && client.isNotActive()) {
|
||||
@@ -903,19 +982,6 @@ public final class LoanTransactionValidator {
|
||||
}
|
||||
}
|
||||
|
||||
public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final Loan loan, final LoanTransactionType loanTransactionType,
|
||||
final LocalDate transactionDate) {
|
||||
if (loanTransactionType.isRepaymentType() && !loanTransactionType.isChargeRefund()) {
|
||||
for (LoanTransaction txn : loan.getLoanTransactions()) {
|
||||
if (txn.isChargeRefund() && DateUtils.isBefore(transactionDate, txn.getTransactionDate())) {
|
||||
final String errorMessage = "loan.transaction.cant.be.created.because.later.charge.refund.exists";
|
||||
final String details = "Loan Transaction: " + loan.getId() + " Can't be created because a Later Charge Refund Exists.";
|
||||
throw new LoanChargeRefundException(errorMessage, details);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateTransactionNotBeforeLastTransactionDate(final Loan loan, LoanTransactionType loanTransactionType,
|
||||
final LocalDate transactionDate) {
|
||||
if (!((LoanScheduleType.CUMULATIVE.equals(loan.getLoanProductRelatedDetail().getLoanScheduleType())
|
||||
@@ -947,4 +1013,26 @@ public final class LoanTransactionValidator {
|
||||
throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, lastTransactionDate);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateIfTransactionIsChargeback(final LoanTransaction chargebackTransaction) {
|
||||
if (!chargebackTransaction.isChargeback()) {
|
||||
final String errorMessage = "A transaction of type chargeback was expected but not received.";
|
||||
throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.chargeback.transaction", errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateLoanRescheduleDate(final Loan loan) {
|
||||
if (DateUtils.isBefore(loan.getRescheduledOnDate(), loan.getDisbursementDate())) {
|
||||
final String errorMessage = "The date on which a loan is rescheduled cannot be before the loan disbursement date: "
|
||||
+ loan.getDisbursementDate().toString();
|
||||
throw new InvalidLoanStateTransitionException("close.reschedule", "cannot.be.before.submittal.date", errorMessage,
|
||||
loan.getRescheduledOnDate(), loan.getDisbursementDate());
|
||||
}
|
||||
|
||||
if (DateUtils.isDateInTheFuture(loan.getRescheduledOnDate())) {
|
||||
final String errorMessage = "The date on which a loan is rescheduled cannot be in the future.";
|
||||
throw new InvalidLoanStateTransitionException("close.reschedule", "cannot.be.a.future.date", errorMessage,
|
||||
loan.getRescheduledOnDate());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -35,6 +35,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleIns
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -48,6 +49,7 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi
|
||||
private final LoanWritePlatformService loanWritePlatformService;
|
||||
private final ExternalIdFactory externalIdFactory;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final LoanChargeValidator loanChargeValidator;
|
||||
|
||||
@Override
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
@@ -92,6 +94,7 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi
|
||||
}
|
||||
|
||||
private void reverseAccrualActivityTransaction(LoanTransaction loanTransaction) {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, "reversed");
|
||||
loanTransaction.reverse();
|
||||
LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
|
||||
|
||||
+20
@@ -56,12 +56,14 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNo
|
||||
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.organisation.office.domain.OfficeRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.AccrualChargeData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodsData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails;
|
||||
@@ -76,6 +78,7 @@ import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -99,6 +102,9 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
|
||||
private final LoanTransactionRepository loanTransactionRepository;
|
||||
private final LoanScheduleGeneratorFactory loanScheduleFactory;
|
||||
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
|
||||
private final OfficeRepository officeRepository;
|
||||
private final LoanChargeRepository loanChargeRepository;
|
||||
private final LoanChargeValidator loanChargeValidator;
|
||||
|
||||
/**
|
||||
* method adds accrual for batch job "Add Periodic Accrual Transactions" and add accruals api for Loan
|
||||
@@ -735,6 +741,8 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
|
||||
for (LoanTransaction loanTransaction : accruals) {
|
||||
if (loanTransaction.getInterestPortion(loan.getCurrency()).isGreaterThanZero()) {
|
||||
if (loanTransaction.getInterestPortion(loan.getCurrency()).isNotEqualTo(interestApplied)) {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction,
|
||||
"reversed");
|
||||
loanTransaction.reverse();
|
||||
if (isExternalIdAutoGenerationEnabled) {
|
||||
externalId = ExternalId.generate();
|
||||
@@ -749,6 +757,8 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
|
||||
LoanCharge loanCharge = chargePaidBy.getLoanCharge();
|
||||
Money chargeAmount = loanCharge.getAmount(loan.getCurrency());
|
||||
if (chargeAmount.isNotEqualTo(loanTransaction.getAmount(loan.getCurrency()))) {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(),
|
||||
loanTransaction, "reversed");
|
||||
loanTransaction.reverse();
|
||||
loan.handleChargeAppliedTransaction(loanCharge, loanTransaction.getTransactionDate());
|
||||
}
|
||||
@@ -776,6 +786,8 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
|
||||
interest = interest.minus(accrualTransaction.getInterestPortion(currency));
|
||||
fee = fee.minus(accrualTransaction.getFeeChargesPortion(currency));
|
||||
penalty = penalty.minus(accrualTransaction.getPenaltyChargesPortion(currency));
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(accrualTransaction.getLoan(),
|
||||
accrualTransaction, "reversed");
|
||||
accrualTransaction.reverse();
|
||||
}
|
||||
}
|
||||
@@ -857,6 +869,8 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
|
||||
compoundingDetail.getAmount(), interest, fee, penalties, externalId);
|
||||
loan.addLoanTransaction(transaction);
|
||||
} else if (existingIncomeTransaction.getAmount(loan.getCurrency()).getAmount().compareTo(compoundingDetail.getAmount()) != 0) {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(existingIncomeTransaction.getLoan(),
|
||||
existingIncomeTransaction, "reversed");
|
||||
existingIncomeTransaction.reverse();
|
||||
LoanTransaction transaction = LoanTransaction.incomePosting(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(),
|
||||
compoundingDetail.getAmount(), interest, fee, penalties, externalId);
|
||||
@@ -875,6 +889,8 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
|
||||
if (existingAccrualTransaction == null
|
||||
|| !MathUtil.isEqualTo(existingAccrualTransaction.getAmount(), compoundingDetail.getAmount())) {
|
||||
if (existingAccrualTransaction != null) {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(existingAccrualTransaction.getLoan(),
|
||||
existingAccrualTransaction, "reversed");
|
||||
existingAccrualTransaction.reverse();
|
||||
}
|
||||
LoanTransaction accrual = LoanTransaction.accrueTransaction(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(),
|
||||
@@ -1176,6 +1192,8 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
|
||||
boolean reversed = false;
|
||||
for (LoanTransaction loanTransaction : transactions) {
|
||||
if (DateUtils.isAfter(loanTransaction.getTransactionDate(), effectiveDate)) {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction,
|
||||
"reversed");
|
||||
reverseAccrual(loanTransaction);
|
||||
reversed = true;
|
||||
}
|
||||
@@ -1187,6 +1205,8 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
|
||||
boolean reversed = false;
|
||||
for (LoanTransaction loanTransaction : transactions) {
|
||||
if (!DateUtils.isBefore(loanTransaction.getTransactionDate(), date)) {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction,
|
||||
"reversed");
|
||||
reverseAccrual(loanTransaction);
|
||||
reversed = true;
|
||||
}
|
||||
|
||||
+5
-1
@@ -85,6 +85,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplica
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleAssembler;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
|
||||
import org.apache.fineract.portfolio.note.domain.Note;
|
||||
@@ -124,6 +125,8 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
|
||||
private final GSIMReadPlatformService gsimReadPlatformService;
|
||||
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
|
||||
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
|
||||
private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator;
|
||||
private final LoanScheduleService loanScheduleService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -615,6 +618,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
|
||||
Loan loan = retrieveLoanBy(loanId);
|
||||
loanApplicationTransitionValidator.checkClientOrGroupActive(loan);
|
||||
|
||||
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_APPROVAL_UNDO);
|
||||
final Map<String, Object> changes = loan.undoApproval(defaultLoanLifecycleStateMachine);
|
||||
if (!changes.isEmpty()) {
|
||||
|
||||
@@ -625,7 +629,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
|
||||
|| changes.containsKey(LoanApiConstants.disbursementPrincipalParameterName)) {
|
||||
LocalDate recalculateFrom = null;
|
||||
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
|
||||
loan.regenerateRepaymentSchedule(scheduleGeneratorDTO);
|
||||
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
}
|
||||
|
||||
|
||||
+7
-4
@@ -135,6 +135,9 @@ public class LoanAssemblerImpl implements LoanAssembler {
|
||||
private final LoanChargeMapper loanChargeMapper;
|
||||
private final LoanCollateralManagementMapper loanCollateralManagementMapper;
|
||||
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
|
||||
private final LoanDisbursementService loanDisbursementService;
|
||||
private final LoanChargeService loanChargeService;
|
||||
private final LoanOfficerService loanOfficerService;
|
||||
|
||||
@Override
|
||||
public Loan assembleFrom(final Long accountId) {
|
||||
@@ -276,7 +279,7 @@ public class LoanAssemblerImpl implements LoanAssembler {
|
||||
loanApplication.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper,
|
||||
this.loanRepaymentScheduleTransactionProcessorFactory);
|
||||
// TODO: review
|
||||
loanApplication.recalculateAllCharges();
|
||||
loanChargeService.recalculateAllCharges(loanApplication);
|
||||
topUpLoanConfiguration(element, loanApplication);
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loanApplication);
|
||||
return loanApplication;
|
||||
@@ -608,7 +611,7 @@ public class LoanAssemblerImpl implements LoanAssembler {
|
||||
final Long newValue = command.longValueOfParameterNamed(LoanApiConstants.loanOfficerIdParameterName);
|
||||
changes.put(LoanApiConstants.loanOfficerIdParameterName, newValue);
|
||||
final Staff newOfficer = findLoanOfficerByIdIfProvided(newValue);
|
||||
loan.updateLoanOfficerOnLoanApplication(newOfficer);
|
||||
loanOfficerService.updateLoanOfficerOnLoanApplication(loan, newOfficer);
|
||||
}
|
||||
|
||||
Long existingLoanPurposeId = null;
|
||||
@@ -718,7 +721,7 @@ public class LoanAssemblerImpl implements LoanAssembler {
|
||||
}
|
||||
|
||||
if (loanProduct.isMultiDisburseLoan()) {
|
||||
loan.updateDisbursementDetails(command, changes);
|
||||
loanDisbursementService.updateDisbursementDetails(loan, command, changes);
|
||||
if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.maxOutstandingBalanceParameterName,
|
||||
loan.getMaxOutstandingLoanBalance())) {
|
||||
loan.setMaxOutstandingLoanBalance(
|
||||
@@ -841,7 +844,7 @@ public class LoanAssemblerImpl implements LoanAssembler {
|
||||
final LoanScheduleModel loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query, false);
|
||||
loan.updateLoanSchedule(loanSchedule);
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
loan.recalculateAllCharges();
|
||||
loanChargeService.recalculateAllCharges(loan);
|
||||
}
|
||||
|
||||
// Changes to modify loan rates.
|
||||
|
||||
+110
-4
@@ -105,6 +105,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtio
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanOverdueInstallmentCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTrancheDisbursementCharge;
|
||||
@@ -127,6 +128,8 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultSche
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanproduct.data.LoanOverdueDTO;
|
||||
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
|
||||
import org.apache.fineract.portfolio.note.domain.Note;
|
||||
@@ -167,6 +170,9 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
private final NoteRepository noteRepository;
|
||||
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
|
||||
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
|
||||
private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator;
|
||||
private final LoanChargeValidator loanChargeValidator;
|
||||
private final LoanScheduleService loanScheduleService;
|
||||
|
||||
private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) {
|
||||
return DateUtils.isAfter(loanCharge.getDueDate(), e.getFromDate()) && !DateUtils.isAfter(loanCharge.getDueDate(), e.getDueDate());
|
||||
@@ -454,6 +460,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
}
|
||||
businessEventNotifierService.notifyPreBusinessEvent(new LoanUpdateChargeBusinessEvent(loanCharge));
|
||||
|
||||
loanChargeValidator.validateLoanIsNotClosed(loan, loanCharge);
|
||||
final Map<String, Object> changes = loan.updateLoanCharge(loanCharge, command);
|
||||
|
||||
this.loanRepositoryWrapper.save(loan);
|
||||
@@ -538,7 +545,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
}
|
||||
}
|
||||
|
||||
final LoanTransaction waiveTransaction = loan.waiveLoanCharge(loanCharge, defaultLoanLifecycleStateMachine, changes,
|
||||
loanChargeValidator.validateLoanIsNotClosed(loan, loanCharge);
|
||||
final LoanTransaction waiveTransaction = waiveLoanCharge(loan, loanCharge, defaultLoanLifecycleStateMachine, changes,
|
||||
existingTransactionIds, existingReversedTransactionIds, loanInstallmentNumber, scheduleGeneratorDTO, accruedCharge,
|
||||
externalId);
|
||||
|
||||
@@ -588,6 +596,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
}
|
||||
businessEventNotifierService.notifyPreBusinessEvent(new LoanDeleteChargeBusinessEvent(loanCharge));
|
||||
|
||||
loanChargeValidator.validateLoanIsNotClosed(loan, loanCharge);
|
||||
loanChargeValidator.validateLoanChargeIsNotWaived(loan, loanCharge);
|
||||
loan.removeLoanCharge(loanCharge);
|
||||
this.loanRepositoryWrapper.save(loan);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanDeleteChargeBusinessEvent(loanCharge));
|
||||
@@ -890,6 +900,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
throw new LoanChargeWaiveCannotBeReversedException(
|
||||
LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason.NOT_WAIVED, loanCharge.getId());
|
||||
}
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction,
|
||||
"reversed");
|
||||
// Reverse waived transaction
|
||||
loanTransaction.reverse();
|
||||
// Set manually adjusted value to `1`
|
||||
@@ -936,6 +948,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
throw new LoanChargeWaiveCannotBeReversedException(
|
||||
LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason.NOT_WAIVED, loanCharge.getId());
|
||||
}
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, "reversed");
|
||||
loanTransaction.reverse();
|
||||
loanTransaction.setManuallyAdjustedOrReversed();
|
||||
loanCharge.setOutstandingAmount(loanCharge.amountOutstanding().add(amountWaived));
|
||||
@@ -1025,6 +1038,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
}
|
||||
}
|
||||
|
||||
loanChargeValidator.validateChargeAdditionForDisbursedLoan(loan, loanCharge);
|
||||
loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate());
|
||||
loan.addLoanCharge(loanCharge);
|
||||
loanCharge = this.loanChargeRepository.saveAndFlush(loanCharge);
|
||||
|
||||
@@ -1139,8 +1154,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
final List<Long> existingTransactionIds = loan.findExistingTransactionIds();
|
||||
ScheduleGeneratorDTO generatorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
|
||||
ChangedTransactionDetail changedTransactionDetail = loan
|
||||
.handleRegenerateRepaymentScheduleWithInterestRecalculation(generatorDTO);
|
||||
ChangedTransactionDetail changedTransactionDetail = loanScheduleService
|
||||
.handleRegenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO);
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
|
||||
|
||||
@@ -1313,7 +1328,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
"Transaction amount cannot be higher than the available charge amount for adjustment: " + availableAmountForAdjustment);
|
||||
}
|
||||
checkClientOrGroupActive(loan);
|
||||
loan.validateAccountStatus(LoanEvent.LOAN_CHARGE_ADJUSTMENT);
|
||||
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_CHARGE_ADJUSTMENT);
|
||||
}
|
||||
|
||||
private BigDecimal calculateAvailableAmountForChargeAdjustment(final LoanCharge loanCharge) {
|
||||
@@ -1356,4 +1371,95 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public LoanTransaction waiveLoanCharge(final Loan loan, final LoanCharge loanCharge,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map<String, Object> changes,
|
||||
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, final Integer loanInstallmentNumber,
|
||||
final ScheduleGeneratorDTO scheduleGeneratorDTO, final Money accruedCharge, final ExternalId externalId) {
|
||||
final Money amountWaived = loanCharge.waive(loan.getCurrency(), loanInstallmentNumber);
|
||||
changes.put("amount", amountWaived.getAmount());
|
||||
|
||||
Money unrecognizedIncome = amountWaived.zero();
|
||||
Money chargeComponent = amountWaived;
|
||||
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
|
||||
Money receivableCharge;
|
||||
if (loanInstallmentNumber != null) {
|
||||
receivableCharge = accruedCharge
|
||||
.minus(loanCharge.getInstallmentLoanCharge(loanInstallmentNumber).getAmountPaid(loan.getCurrency()));
|
||||
} else {
|
||||
receivableCharge = accruedCharge.minus(loanCharge.getAmountPaid(loan.getCurrency()));
|
||||
}
|
||||
if (receivableCharge.isLessThanZero()) {
|
||||
receivableCharge = amountWaived.zero();
|
||||
}
|
||||
if (amountWaived.isGreaterThan(receivableCharge)) {
|
||||
chargeComponent = receivableCharge;
|
||||
unrecognizedIncome = amountWaived.minus(receivableCharge);
|
||||
}
|
||||
}
|
||||
Money feeChargesWaived = chargeComponent;
|
||||
Money penaltyChargesWaived = Money.zero(loan.getCurrency());
|
||||
if (loanCharge.isPenaltyCharge()) {
|
||||
penaltyChargesWaived = chargeComponent;
|
||||
feeChargesWaived = Money.zero(loan.getCurrency());
|
||||
}
|
||||
|
||||
LocalDate transactionDate = loan.getDisbursementDate();
|
||||
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
|
||||
if (loanCharge.isDueDateCharge()) {
|
||||
if (DateUtils.isAfter(loanCharge.getDueLocalDate(), businessDate)) {
|
||||
transactionDate = businessDate;
|
||||
} else {
|
||||
transactionDate = loanCharge.getDueLocalDate();
|
||||
}
|
||||
} else if (loanCharge.isInstalmentFee()) {
|
||||
LocalDate repaymentDueDate = loanCharge.getInstallmentLoanCharge(loanInstallmentNumber).getRepaymentInstallment().getDueDate();
|
||||
if (DateUtils.isAfter(repaymentDueDate, businessDate)) {
|
||||
transactionDate = businessDate;
|
||||
} else {
|
||||
transactionDate = repaymentDueDate;
|
||||
}
|
||||
}
|
||||
|
||||
scheduleGeneratorDTO.setRecalculateFrom(transactionDate);
|
||||
|
||||
loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement());
|
||||
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
|
||||
final LoanTransaction waiveLoanChargeTransaction = LoanTransaction.waiveLoanCharge(loan, loan.getOffice(), amountWaived,
|
||||
transactionDate, feeChargesWaived, penaltyChargesWaived, unrecognizedIncome, externalId);
|
||||
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(waiveLoanChargeTransaction, loanCharge,
|
||||
waiveLoanChargeTransaction.getAmount(loan.getCurrency()).getAmount(), loanInstallmentNumber);
|
||||
waiveLoanChargeTransaction.getLoanChargesPaid().add(loanChargePaidBy);
|
||||
loan.addLoanTransaction(waiveLoanChargeTransaction);
|
||||
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()
|
||||
&& DateUtils.isBefore(loanCharge.getDueLocalDate(), businessDate)) {
|
||||
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
|
||||
}
|
||||
// Waive of charges whose due date falls after latest 'repayment' transaction don't require entire loan schedule
|
||||
// to be reprocessed.
|
||||
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loan.getCurrency())) {
|
||||
/*
|
||||
* TODO Vishwas Currently we do not allow waiving fully paid loan charge and waiving partially paid loan
|
||||
* charges only waives the remaining amount.
|
||||
*
|
||||
* Consider removing this block of code or logically completing it for the future by getting the list of
|
||||
* affected Transactions
|
||||
*/
|
||||
loan.reprocessTransactions();
|
||||
} else {
|
||||
// reprocess loan schedule based on charge been waived.
|
||||
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
|
||||
wrapper.reprocess(loan.getCurrency(), loan.getDisbursementDate(), loan.getRepaymentScheduleInstallments(),
|
||||
loan.getActiveCharges());
|
||||
}
|
||||
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
|
||||
loan.doPostLoanTransactionChecks(waiveLoanChargeTransaction.getTransactionDate(), loanLifecycleStateMachine);
|
||||
|
||||
return waiveLoanChargeTransaction;
|
||||
}
|
||||
}
|
||||
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* 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 static org.apache.fineract.portfolio.loanaccount.domain.Loan.RECALCULATE_LOAN_SCHEDULE;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.serialization.JsonParserHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
|
||||
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTrancheCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTrancheDisbursementCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanDisbursementValidator;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class LoanDisbursementService {
|
||||
|
||||
private final LoanChargeValidator loanChargeValidator;
|
||||
private final LoanDisbursementValidator loanDisbursementValidator;
|
||||
|
||||
public void updateDisbursementDetails(final Loan loan, final JsonCommand jsonCommand, final Map<String, Object> actualChanges) {
|
||||
final List<Long> disbursementList = loan.fetchDisbursementIds();
|
||||
final List<Long> loanChargeIds = loan.fetchLoanTrancheChargeIds();
|
||||
final int chargeIdLength = loanChargeIds.size();
|
||||
String chargeIds;
|
||||
// From modify application page, if user removes all charges, we should
|
||||
// get empty array.
|
||||
// So we need to remove all charges applied for this loan
|
||||
final boolean removeAllCharges = jsonCommand.parameterExists(LoanApiConstants.chargesParameterName)
|
||||
&& jsonCommand.arrayOfParameterNamed(LoanApiConstants.chargesParameterName).isEmpty();
|
||||
|
||||
if (jsonCommand.parameterExists(LoanApiConstants.disbursementDataParameterName)) {
|
||||
final JsonArray disbursementDataArray = jsonCommand.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName);
|
||||
if (disbursementDataArray != null && !disbursementDataArray.isEmpty()) {
|
||||
String dateFormat;
|
||||
Locale locale = null;
|
||||
final Map<String, String> dateAndLocale = loan.getDateFormatAndLocale(jsonCommand);
|
||||
dateFormat = dateAndLocale.get(LoanApiConstants.dateFormatParameterName);
|
||||
if (dateAndLocale.containsKey(LoanApiConstants.localeParameterName)) {
|
||||
locale = JsonParserHelper.localeFromString(dateAndLocale.get(LoanApiConstants.localeParameterName));
|
||||
}
|
||||
for (JsonElement jsonElement : disbursementDataArray) {
|
||||
final JsonObject jsonObject = jsonElement.getAsJsonObject();
|
||||
final Map<String, Object> parsedDisbursementData = loan.parseDisbursementDetails(jsonObject, dateFormat, locale);
|
||||
final LocalDate expectedDisbursementDate = (LocalDate) parsedDisbursementData
|
||||
.get(LoanApiConstants.expectedDisbursementDateParameterName);
|
||||
final BigDecimal principal = (BigDecimal) parsedDisbursementData
|
||||
.get(LoanApiConstants.disbursementPrincipalParameterName);
|
||||
final Long disbursementID = (Long) parsedDisbursementData.get(LoanApiConstants.disbursementIdParameterName);
|
||||
chargeIds = (String) parsedDisbursementData.get(LoanApiConstants.loanChargeIdParameterName);
|
||||
if (chargeIds != null) {
|
||||
if (chargeIds.contains(",")) {
|
||||
final Iterable<String> chargeId = Splitter.on(',').split(chargeIds);
|
||||
for (String loanChargeId : chargeId) {
|
||||
loanChargeIds.remove(Long.parseLong(loanChargeId));
|
||||
}
|
||||
} else {
|
||||
loanChargeIds.remove(Long.parseLong(chargeIds));
|
||||
}
|
||||
}
|
||||
createOrUpdateDisbursementDetails(loan, disbursementID, actualChanges, expectedDisbursementDate, principal,
|
||||
disbursementList);
|
||||
}
|
||||
removeDisbursementAndAssociatedCharges(loan, actualChanges, disbursementList, loanChargeIds, chargeIdLength,
|
||||
removeAllCharges);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Money adjustDisburseAmount(final Loan loan, @NotNull final JsonCommand command,
|
||||
@NotNull final LocalDate actualDisbursementDate) {
|
||||
Money disburseAmount = loan.getLoanRepaymentScheduleDetail().getPrincipal().zero();
|
||||
final BigDecimal principalDisbursed = command.bigDecimalValueOfParameterNamed(LoanApiConstants.principalDisbursedParameterName);
|
||||
if (loan.getActualDisbursementDate() == null || DateUtils.isBefore(actualDisbursementDate, loan.getActualDisbursementDate())) {
|
||||
loan.setActualDisbursementDate(actualDisbursementDate);
|
||||
}
|
||||
BigDecimal diff = BigDecimal.ZERO;
|
||||
final Collection<LoanDisbursementDetails> details = loan.fetchUndisbursedDetail();
|
||||
if (principalDisbursed == null) {
|
||||
disburseAmount = loan.getLoanRepaymentScheduleDetail().getPrincipal();
|
||||
if (!details.isEmpty()) {
|
||||
disburseAmount = disburseAmount.zero();
|
||||
for (LoanDisbursementDetails disbursementDetails : details) {
|
||||
disbursementDetails.updateActualDisbursementDate(actualDisbursementDate);
|
||||
disburseAmount = disburseAmount.plus(disbursementDetails.principal());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (loan.getLoanProduct().isMultiDisburseLoan()) {
|
||||
disburseAmount = Money.of(loan.getCurrency(), principalDisbursed);
|
||||
} else {
|
||||
disburseAmount = disburseAmount.plus(principalDisbursed);
|
||||
}
|
||||
|
||||
if (details.isEmpty()) {
|
||||
diff = loan.getLoanRepaymentScheduleDetail().getPrincipal().minus(principalDisbursed).getAmount();
|
||||
} else {
|
||||
for (LoanDisbursementDetails disbursementDetails : details) {
|
||||
disbursementDetails.updateActualDisbursementDate(actualDisbursementDate);
|
||||
disbursementDetails.updatePrincipal(principalDisbursed);
|
||||
}
|
||||
}
|
||||
if (loan.loanProduct().isMultiDisburseLoan()) {
|
||||
Collection<LoanDisbursementDetails> loanDisburseDetails = loan.getDisbursementDetails();
|
||||
BigDecimal setPrincipalAmount = BigDecimal.ZERO;
|
||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||
for (LoanDisbursementDetails disbursementDetails : loanDisburseDetails) {
|
||||
if (disbursementDetails.actualDisbursementDate() != null) {
|
||||
setPrincipalAmount = setPrincipalAmount.add(disbursementDetails.principal());
|
||||
}
|
||||
totalAmount = totalAmount.add(disbursementDetails.principal());
|
||||
}
|
||||
loan.getLoanRepaymentScheduleDetail().setPrincipal(setPrincipalAmount);
|
||||
loanDisbursementValidator.compareDisbursedToApprovedOrProposedPrincipal(loan, disburseAmount.getAmount(), totalAmount);
|
||||
} else {
|
||||
loan.getLoanRepaymentScheduleDetail()
|
||||
.setPrincipal(loan.getLoanRepaymentScheduleDetail().getPrincipal().minus(diff).getAmount());
|
||||
}
|
||||
loanDisbursementValidator.validateDisburseAmountNotExceedingApprovedAmount(loan, diff, principalDisbursed);
|
||||
}
|
||||
return disburseAmount;
|
||||
}
|
||||
|
||||
public void handleDisbursementTransaction(final Loan loan, final LocalDate disbursedOn, final PaymentDetail paymentDetail) {
|
||||
// add repayment transaction to track incoming money from client to mfi
|
||||
// for (charges due at time of disbursement)
|
||||
|
||||
/*
|
||||
* TODO Vishwas: do we need to be able to pass in payment type details for repayments at disbursements too?
|
||||
*/
|
||||
|
||||
final Money totalFeeChargesDueAtDisbursement = loan.getSummary().getTotalFeeChargesDueAtDisbursement(loan.getCurrency());
|
||||
/*
|
||||
* all Charges repaid at disbursal is marked as repaid and "APPLY Charge" transactions are created for all other
|
||||
* fees ( which are created during disbursal but not repaid)
|
||||
*/
|
||||
|
||||
Money disbursentMoney = Money.zero(loan.getCurrency());
|
||||
final LoanTransaction chargesPayment = LoanTransaction.repaymentAtDisbursement(loan.getOffice(), disbursentMoney, paymentDetail,
|
||||
disbursedOn, null);
|
||||
final Integer installmentNumber = null;
|
||||
for (final LoanCharge charge : loan.getActiveCharges()) {
|
||||
LocalDate actualDisbursementDate = loan.getActualDisbursementDate(charge);
|
||||
/*
|
||||
* create a Charge applied transaction if Up front Accrual, None or Cash based accounting is enabled
|
||||
*/
|
||||
if ((charge.getCharge().getChargeTimeType().equals(ChargeTimeType.DISBURSEMENT.getValue())
|
||||
&& disbursedOn.equals(actualDisbursementDate) && !charge.isWaived() && !charge.isFullyPaid())
|
||||
|| (charge.getCharge().getChargeTimeType().equals(ChargeTimeType.TRANCHE_DISBURSEMENT.getValue())
|
||||
&& disbursedOn.equals(actualDisbursementDate) && !charge.isWaived() && !charge.isFullyPaid())) {
|
||||
if (totalFeeChargesDueAtDisbursement.isGreaterThanZero() && !charge.getChargePaymentMode().isPaymentModeAccountTransfer()) {
|
||||
charge.markAsFullyPaid();
|
||||
// Add "Loan Charge Paid By" details to this transaction
|
||||
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge, charge.amount(),
|
||||
installmentNumber);
|
||||
chargesPayment.getLoanChargesPaid().add(loanChargePaidBy);
|
||||
disbursentMoney = disbursentMoney.plus(charge.amount());
|
||||
}
|
||||
} else if (disbursedOn.equals(loan.getActualDisbursementDate())
|
||||
&& loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) {
|
||||
loan.handleChargeAppliedTransaction(charge, disbursedOn);
|
||||
}
|
||||
}
|
||||
|
||||
if (disbursentMoney.isGreaterThanZero()) {
|
||||
final Money zero = Money.zero(loan.getCurrency());
|
||||
chargesPayment.updateComponentsAndTotal(zero, zero, disbursentMoney, zero);
|
||||
chargesPayment.updateLoan(loan);
|
||||
loan.addLoanTransaction(chargesPayment);
|
||||
loan.updateLoanOutstandingBalances();
|
||||
}
|
||||
|
||||
final LocalDate expectedDate = loan.getExpectedFirstRepaymentOnDate();
|
||||
loanDisbursementValidator.validateDisburseDate(loan, disbursedOn, expectedDate);
|
||||
}
|
||||
|
||||
private void createOrUpdateDisbursementDetails(final Loan loan, final Long disbursementID, final Map<String, Object> actualChanges,
|
||||
final LocalDate expectedDisbursementDate, final BigDecimal principal, final List<Long> existingDisbursementList) {
|
||||
if (disbursementID != null) {
|
||||
LoanDisbursementDetails loanDisbursementDetail = loan.fetchLoanDisbursementsById(disbursementID);
|
||||
existingDisbursementList.remove(disbursementID);
|
||||
if (loanDisbursementDetail.actualDisbursementDate() == null) {
|
||||
LocalDate actualDisbursementDate = null;
|
||||
LoanDisbursementDetails disbursementDetails = new LoanDisbursementDetails(expectedDisbursementDate, actualDisbursementDate,
|
||||
principal, loan.getNetDisbursalAmount(), false);
|
||||
disbursementDetails.updateLoan(loan);
|
||||
if (!loanDisbursementDetail.equals(disbursementDetails)) {
|
||||
loanDisbursementDetail.copy(disbursementDetails);
|
||||
actualChanges.put("disbursementDetailId", disbursementID);
|
||||
actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final var disbursementDetails = loan.addLoanDisbursementDetails(expectedDisbursementDate, principal);
|
||||
for (LoanTrancheCharge trancheCharge : loan.getTrancheCharges()) {
|
||||
Charge chargeDefinition = trancheCharge.getCharge();
|
||||
ExternalId externalId = ExternalId.empty();
|
||||
if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) {
|
||||
externalId = ExternalId.generate();
|
||||
}
|
||||
final LoanCharge loanCharge = new LoanCharge(loan, chargeDefinition, principal, null, null, null, expectedDisbursementDate,
|
||||
null, null, BigDecimal.ZERO, externalId);
|
||||
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge,
|
||||
disbursementDetails);
|
||||
loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge);
|
||||
|
||||
loanChargeValidator.validateChargeAdditionForDisbursedLoan(loan, loanCharge);
|
||||
loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate());
|
||||
loan.addLoanCharge(loanCharge);
|
||||
}
|
||||
actualChanges.put(LoanApiConstants.disbursementDataParameterName, expectedDisbursementDate + "-" + principal);
|
||||
actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeDisbursementAndAssociatedCharges(final Loan loan, final Map<String, Object> actualChanges,
|
||||
final List<Long> disbursementList, final List<Long> loanChargeIds, final int chargeIdLength, final boolean removeAllCharges) {
|
||||
if (removeAllCharges) {
|
||||
final LoanCharge[] tempCharges = new LoanCharge[loan.getCharges().size()];
|
||||
loan.getCharges().toArray(tempCharges);
|
||||
for (LoanCharge loanCharge : tempCharges) {
|
||||
loanChargeValidator.validateLoanIsNotClosed(loan, loanCharge);
|
||||
loanChargeValidator.validateLoanChargeIsNotWaived(loan, loanCharge);
|
||||
loan.removeLoanCharge(loanCharge);
|
||||
}
|
||||
loan.getTrancheCharges().clear();
|
||||
} else {
|
||||
if (!loanChargeIds.isEmpty() && loanChargeIds.size() != chargeIdLength) {
|
||||
for (Long chargeId : loanChargeIds) {
|
||||
final LoanCharge deleteCharge = loan.fetchLoanChargesById(chargeId);
|
||||
if (loan.getCharges().contains(deleteCharge)) {
|
||||
loanChargeValidator.validateLoanIsNotClosed(loan, deleteCharge);
|
||||
loanChargeValidator.validateLoanChargeIsNotWaived(loan, deleteCharge);
|
||||
loan.removeLoanCharge(deleteCharge);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Long id : disbursementList) {
|
||||
removeChargesByDisbursementID(loan, id);
|
||||
loan.removeDisbursementDetails(id);
|
||||
actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeChargesByDisbursementID(final Loan loan, final Long id) {
|
||||
loan.getCharges().stream() //
|
||||
.filter(charge -> { //
|
||||
final LoanTrancheDisbursementCharge transCharge = charge.getTrancheDisbursementCharge(); //
|
||||
if (transCharge == null || !Objects.equals(id, transCharge.getloanDisbursementDetails().getId())) {
|
||||
return false;
|
||||
}
|
||||
loanChargeValidator.validateLoanIsNotClosed(loan, charge); //
|
||||
loanChargeValidator.validateLoanChargeIsNotWaived(loan, charge); //
|
||||
return true; //
|
||||
}) //
|
||||
.forEach(loan::removeLoanCharge);
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.organisation.staff.domain.Staff;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanOfficerAssignmentHistory;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanOfficerValidator;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class LoanOfficerService {
|
||||
|
||||
private final LoanOfficerValidator loanOfficerValidator;
|
||||
|
||||
public void reassignLoanOfficer(final Loan loan, final Staff newLoanOfficer, final LocalDate assignmentDate) {
|
||||
final Optional<LoanOfficerAssignmentHistory> latestHistoryRecord = loan.findLatestIncompleteHistoryRecord();
|
||||
final LoanOfficerAssignmentHistory lastAssignmentRecord = loan.findLastAssignmentHistoryRecord(newLoanOfficer);
|
||||
|
||||
// assignment date should not be less than loan submitted date
|
||||
loanOfficerValidator.validateReassignment(loan, assignmentDate, lastAssignmentRecord);
|
||||
loanOfficerValidator.validateAssignmentDateWithHistory(loan, latestHistoryRecord, assignmentDate);
|
||||
|
||||
if (latestHistoryRecord.isPresent() && loan.getLoanOfficer().identifiedBy(newLoanOfficer)) {
|
||||
latestHistoryRecord.get().updateStartDate(assignmentDate);
|
||||
} else if (latestHistoryRecord.isPresent() && latestHistoryRecord.get().matchesStartDateOf(assignmentDate)) {
|
||||
latestHistoryRecord.get().updateLoanOfficer(newLoanOfficer);
|
||||
loan.setLoanOfficer(newLoanOfficer);
|
||||
} else {
|
||||
// loan officer correctly changed from previous loan officer to new loan officer
|
||||
latestHistoryRecord.ifPresent(loanOfficerAssignmentHistory -> loanOfficerAssignmentHistory.updateEndDate(assignmentDate));
|
||||
|
||||
loan.setLoanOfficer(newLoanOfficer);
|
||||
if (loan.isNotSubmittedAndPendingApproval()) {
|
||||
final LoanOfficerAssignmentHistory loanOfficerAssignmentHistory = LoanOfficerAssignmentHistory.createNew(loan,
|
||||
loan.getLoanOfficer(), assignmentDate);
|
||||
loan.getLoanOfficerHistory().add(loanOfficerAssignmentHistory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateLoanOfficerOnLoanApplication(final Loan loan, final Staff newLoanOfficer) {
|
||||
if (!loan.isSubmittedAndPendingApproval()) {
|
||||
Long loanOfficerId = null;
|
||||
if (loan.getLoanOfficer() != null) {
|
||||
loanOfficerId = loan.getLoanOfficer().getId();
|
||||
}
|
||||
throw new LoanOfficerAssignmentException(loan.getId(), loanOfficerId);
|
||||
}
|
||||
loan.setLoanOfficer(newLoanOfficer);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -118,6 +118,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleP
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanForeclosureValidator;
|
||||
import org.apache.fineract.portfolio.loanproduct.data.LoanProductData;
|
||||
import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
|
||||
@@ -170,6 +171,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
private final LoanTransactionRepository loanTransactionRepository;
|
||||
private final LoanChargePaidByReadService loanChargePaidByReadService;
|
||||
private final LoanTransactionRelationReadService loanTransactionRelationReadService;
|
||||
private final LoanForeclosureValidator loanForeclosureValidator;
|
||||
|
||||
@Override
|
||||
public LoanAccountData retrieveOne(final Long loanId) {
|
||||
@@ -2028,7 +2030,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
this.context.authenticatedUser();
|
||||
|
||||
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
|
||||
loan.validateForForeclosure(transactionDate);
|
||||
loanForeclosureValidator.validateForForeclosure(loan, transactionDate);
|
||||
final MonetaryCurrency currency = loan.getCurrency();
|
||||
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
|
||||
|
||||
|
||||
+524
-40
@@ -18,6 +18,13 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.service;
|
||||
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.ACTUAL_DISBURSEMENT_DATE;
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.CLOSED_ON_DATE;
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.EXTERNAL_ID;
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.PARAM_STATUS;
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.TRANSACTION_DATE;
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.WRITTEN_OFF_ON_DATE;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
@@ -30,12 +37,15 @@ import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -156,6 +166,7 @@ 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.LoanRepaymentScheduleInstallmentRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||
@@ -175,6 +186,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Mon
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException;
|
||||
@@ -184,6 +196,7 @@ import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerUnassignme
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataNotAllowedException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.UndoLastTrancheDisbursementException;
|
||||
import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorDomainService;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
|
||||
@@ -191,6 +204,9 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanOfficerValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
|
||||
@@ -263,6 +279,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
private final AccountTransferRepository accountTransferRepository;
|
||||
private final LoanTransactionAssembler loanTransactionAssembler;
|
||||
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
|
||||
private final LoanOfficerValidator loanOfficerValidator;
|
||||
private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator;
|
||||
private final LoanDisbursementService loanDisbursementService;
|
||||
private final LoanScheduleService loanScheduleService;
|
||||
private final LoanChargeValidator loanChargeValidator;
|
||||
private final LoanOfficerService loanOfficerService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -354,7 +376,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
if (netDisbursalAmount != null) {
|
||||
loan.setNetDisbursalAmount(netDisbursalAmount);
|
||||
}
|
||||
Money disburseAmount = loan.adjustDisburseAmount(command, actualDisbursementDate);
|
||||
Money disburseAmount = loanDisbursementService.adjustDisburseAmount(loan, command, actualDisbursementDate);
|
||||
Money amountToDisburse = disburseAmount.copy();
|
||||
boolean recalculateSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincipal());
|
||||
final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
|
||||
@@ -556,25 +578,27 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
PaymentDetail paymentDetail, Loan loan, AppUser currentUser, Map<String, Object> changes,
|
||||
ScheduleGeneratorDTO scheduleGeneratorDTO) {
|
||||
final PaymentDetail paymentDetail1 = isPaymentTypeApplicableForDisbursementCharge ? paymentDetail : null;
|
||||
final LocalDate actualDisbursementDate1 = command.localDateValueOfParameterNamed(Loan.ACTUAL_DISBURSEMENT_DATE);
|
||||
final LocalDate actualDisbursementDate1 = command.localDateValueOfParameterNamed(ACTUAL_DISBURSEMENT_DATE);
|
||||
|
||||
loan.setDisbursedBy(currentUser);
|
||||
loan.updateLoanScheduleDependentDerivedFields();
|
||||
|
||||
changes.put(Loan.LOCALE, command.locale());
|
||||
changes.put(Loan.DATE_FORMAT, command.dateFormat());
|
||||
changes.put(Loan.ACTUAL_DISBURSEMENT_DATE, command.stringValueOfParameterNamed(Loan.ACTUAL_DISBURSEMENT_DATE));
|
||||
changes.put(ACTUAL_DISBURSEMENT_DATE, command.stringValueOfParameterNamed(ACTUAL_DISBURSEMENT_DATE));
|
||||
|
||||
boolean disbursementMissedParam = loan.isDisbursementMissed();
|
||||
LocalDate firstInstallmentDueDate = loan.fetchRepaymentScheduleInstallment(1).getDueDate();
|
||||
if ((loan.repaymentScheduleDetail().isInterestRecalculationEnabled()
|
||||
&& (DateUtils.isBeforeBusinessDate(firstInstallmentDueDate) || disbursementMissedParam))) {
|
||||
loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
|
||||
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
|
||||
}
|
||||
|
||||
loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement());
|
||||
loan.updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate1);
|
||||
loan.handleDisbursementTransaction(actualDisbursementDate1, paymentDetail1);
|
||||
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_DISBURSED,
|
||||
actualDisbursementDate1);
|
||||
loanDisbursementService.handleDisbursementTransaction(loan, actualDisbursementDate1, paymentDetail1);
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
final Money interestApplied = Money.of(loan.getCurrency(), loan.getSummary().getTotalInterestCharged());
|
||||
|
||||
@@ -603,7 +627,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
}
|
||||
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, loan);
|
||||
changes.put(Loan.PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus()));
|
||||
changes.put(PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus()));
|
||||
return changedTransactionDetail;
|
||||
}
|
||||
|
||||
@@ -762,13 +786,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
// disbursement date and next available meeting dates
|
||||
// assuming repayment schedule won't regenerate because expected
|
||||
// disbursement and actual disbursement happens on same date
|
||||
loan.validateAccountStatus(LoanEvent.LOAN_DISBURSED);
|
||||
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_DISBURSED);
|
||||
updateLoanCounters(loan, actualDisbursementDate);
|
||||
boolean canDisburse = loan.canDisburse();
|
||||
ChangedTransactionDetail changedTransactionDetail = null;
|
||||
if (canDisburse) {
|
||||
Money amountBeforeAdjust = loan.getPrincipal();
|
||||
Money disburseAmount = loan.adjustDisburseAmount(command, actualDisbursementDate);
|
||||
Money disburseAmount = loanDisbursementService.adjustDisburseAmount(loan, command, actualDisbursementDate);
|
||||
boolean recalculateSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincipal());
|
||||
final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
|
||||
if (isAccountTransfer) {
|
||||
@@ -901,7 +925,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
// Remove post dated checks if added.
|
||||
loan.removePostDatedChecks();
|
||||
|
||||
final Map<String, Object> changes = loan.undoDisbursal(scheduleGeneratorDTO, existingTransactionIds,
|
||||
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_DISBURSAL_UNDO);
|
||||
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_DISBURSAL_UNDO,
|
||||
loan.getDisbursementDate());
|
||||
final Map<String, Object> changes = undoDisbursal(loan, scheduleGeneratorDTO, existingTransactionIds,
|
||||
existingReversedTransactionIds);
|
||||
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
@@ -1121,7 +1148,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
|
||||
ScheduleGeneratorDTO scheduleGeneratorDTO) {
|
||||
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
|
||||
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
|
||||
}
|
||||
@@ -1137,6 +1164,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
transactionDate);
|
||||
if (!newLoanTransaction.getDateOf().isEqual(loanTransaction.getDateOf())
|
||||
|| !LoanTransaction.transactionAmountsMatch(loan.getCurrency(), loanTransaction, newLoanTransaction)) {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction,
|
||||
"reversed");
|
||||
loanTransaction.reverse();
|
||||
loanTransaction.updateExternalId(null);
|
||||
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
|
||||
@@ -1340,9 +1369,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
|
||||
holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday,
|
||||
allowTransactionsOnNonWorkingDay);
|
||||
loan.validateRepaymentDateIsOnHoliday(singleLoanRepaymentCommand.getTransactionDate(),
|
||||
loanTransactionValidator.validateRepaymentDateIsOnHoliday(singleLoanRepaymentCommand.getTransactionDate(),
|
||||
holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays());
|
||||
loan.validateRepaymentDateIsOnNonWorkingDay(singleLoanRepaymentCommand.getTransactionDate(),
|
||||
loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(singleLoanRepaymentCommand.getTransactionDate(),
|
||||
holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
|
||||
isHolidayValidationDone = true;
|
||||
break;
|
||||
@@ -1578,7 +1607,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
|
||||
loan.validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER,
|
||||
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_REPAYMENT_OR_WAIVER,
|
||||
transactionForAdjustment.getTransactionDate());
|
||||
|
||||
if (transactionForAdjustment.isNotRepaymentLikeType() && transactionForAdjustment.isNotWaiver()
|
||||
@@ -1588,6 +1617,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
"adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.transactions", errorMessage);
|
||||
}
|
||||
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(transactionForAdjustment.getLoan(),
|
||||
transactionForAdjustment, "reversed");
|
||||
transactionForAdjustment.reverse(reversalExternalId);
|
||||
transactionForAdjustment.manuallyAdjustedOrReversed();
|
||||
|
||||
@@ -1599,6 +1630,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
.anyMatch(relation -> relation.getRelationType().equals(LoanTransactionRelationTypeEnum.RELATED)
|
||||
&& relation.getToTransaction().getId().equals(transactionForAdjustment.getId())))
|
||||
.forEach(loanTransaction -> {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(),
|
||||
loanTransaction, "reversed");
|
||||
loanTransaction.reverse();
|
||||
loanTransaction.manuallyAdjustedOrReversed();
|
||||
LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
|
||||
@@ -1609,6 +1642,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
if (loan.isClosedWrittenOff()) {
|
||||
// find write off transaction and reverse it
|
||||
final LoanTransaction writeOffTransaction = loan.findWriteOffTransaction();
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(writeOffTransaction.getLoan(), writeOffTransaction,
|
||||
"reversed");
|
||||
writeOffTransaction.reverse();
|
||||
}
|
||||
|
||||
@@ -1617,8 +1652,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
}
|
||||
|
||||
if (newTransactionDetail.isRepaymentLikeType() || newTransactionDetail.isInterestWaiver()) {
|
||||
changedTransactionDetail = loan.handleRepaymentOrRecoveryOrWaiverTransaction(newTransactionDetail, loanLifecycleStateMachine,
|
||||
transactionForAdjustment, scheduleGeneratorDTO);
|
||||
changedTransactionDetail = loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan,
|
||||
newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment, scheduleGeneratorDTO);
|
||||
}
|
||||
|
||||
return changedTransactionDetail;
|
||||
@@ -1693,7 +1728,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
|
||||
newTransaction = this.loanTransactionRepository.saveAndFlush(newTransaction);
|
||||
|
||||
loan.handleChargebackTransaction(newTransaction, loanLifecycleStateMachine);
|
||||
handleChargebackTransaction(loan, newTransaction, loanLifecycleStateMachine);
|
||||
|
||||
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
||||
|
||||
@@ -1777,7 +1812,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
}
|
||||
|
||||
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
|
||||
final ChangedTransactionDetail changedTransactionDetail = loan.waiveInterest(waiveInterestTransaction, loanLifecycleStateMachine,
|
||||
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_REPAYMENT_OR_WAIVER);
|
||||
loanTransactionValidator.validateActivityNotBeforeLastTransactionDate(loan, waiveInterestTransaction.getTransactionDate(),
|
||||
LoanEvent.LOAN_REPAYMENT_OR_WAIVER);
|
||||
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_REPAYMENT_OR_WAIVER,
|
||||
waiveInterestTransaction.getTransactionDate());
|
||||
final ChangedTransactionDetail changedTransactionDetail = waiveInterest(loan, waiveInterestTransaction, loanLifecycleStateMachine,
|
||||
existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO);
|
||||
|
||||
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
@@ -1874,8 +1914,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
}
|
||||
|
||||
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
|
||||
|
||||
final ChangedTransactionDetail changedTransactionDetail = loan.closeAsWrittenOff(command, loanLifecycleStateMachine, changes,
|
||||
final LocalDate writtenOffOnLocalDate = command.localDateValueOfParameterNamed(TRANSACTION_DATE);
|
||||
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.WRITE_OFF_OUTSTANDING);
|
||||
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.WRITE_OFF_OUTSTANDING,
|
||||
writtenOffOnLocalDate);
|
||||
final ChangedTransactionDetail changedTransactionDetail = closeAsWrittenOff(loan, command, loanLifecycleStateMachine, changes,
|
||||
existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO);
|
||||
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
@@ -1948,8 +1991,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
recalculateFrom = command.localDateValueOfParameterNamed("transactionDate");
|
||||
}
|
||||
|
||||
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
|
||||
ChangedTransactionDetail changedTransactionDetail = loan.close(command, loanLifecycleStateMachine, changes, existingTransactionIds,
|
||||
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
|
||||
final LocalDate closureDate = command.localDateValueOfParameterNamed(TRANSACTION_DATE);
|
||||
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_CLOSED);
|
||||
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.REPAID_IN_FULL, closureDate);
|
||||
ChangedTransactionDetail changedTransactionDetail = close(loan, command, loanLifecycleStateMachine, changes, existingTransactionIds,
|
||||
existingReversedTransactionIds, scheduleGeneratorDTO);
|
||||
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
@@ -2038,7 +2084,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
changes.put("locale", command.locale());
|
||||
changes.put("dateFormat", command.dateFormat());
|
||||
|
||||
loan.closeAsMarkedForReschedule(command, loanLifecycleStateMachine, changes);
|
||||
closeAsMarkedForReschedule(loan, command, loanLifecycleStateMachine, changes);
|
||||
|
||||
saveLoanWithDataIntegrityViolationChecks(loan);
|
||||
|
||||
@@ -2149,7 +2195,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan);
|
||||
}
|
||||
if (loanOfficer != null) {
|
||||
loan.reassignLoanOfficer(loanOfficer, transferDate);
|
||||
loanOfficerService.reassignLoanOfficer(loan, loanOfficer, transferDate);
|
||||
}
|
||||
|
||||
this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction);
|
||||
@@ -2218,7 +2264,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
throw new LoanOfficerAssignmentException(loanId, fromLoanOfficerId);
|
||||
}
|
||||
|
||||
loan.reassignLoanOfficer(toLoanOfficer, dateOfLoanOfficerAssignment);
|
||||
loanOfficerService.reassignLoanOfficer(loan, toLoanOfficer, dateOfLoanOfficerAssignment);
|
||||
|
||||
saveLoanWithDataIntegrityViolationChecks(loan);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanReassignOfficerBusinessEvent(loan));
|
||||
@@ -2263,7 +2309,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
throw new LoanOfficerAssignmentException(loanId, fromLoanOfficerId);
|
||||
}
|
||||
|
||||
loan.reassignLoanOfficer(toLoanOfficer, dateOfLoanOfficerAssignment);
|
||||
loanOfficerService.reassignLoanOfficer(loan, toLoanOfficer, dateOfLoanOfficerAssignment);
|
||||
saveLoanWithDataIntegrityViolationChecks(loan);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanReassignOfficerBusinessEvent(loan));
|
||||
}
|
||||
@@ -2296,6 +2342,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
}
|
||||
businessEventNotifierService.notifyPreBusinessEvent(new LoanRemoveOfficerBusinessEvent(loan));
|
||||
|
||||
loanOfficerValidator.validateUnassignDate(loan, dateOfLoanOfficerUnassigned);
|
||||
loan.removeLoanOfficer(dateOfLoanOfficerUnassigned);
|
||||
|
||||
saveLoanWithDataIntegrityViolationChecks(loan);
|
||||
@@ -2392,7 +2439,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
|
||||
loan.setHelpers(null, this.loanSummaryWrapper, this.transactionProcessingStrategy);
|
||||
loan.recalculateScheduleFromLastTransaction(scheduleGeneratorDTO, existingTransactionIds,
|
||||
loanScheduleService.recalculateScheduleFromLastTransaction(loan, scheduleGeneratorDTO, existingTransactionIds,
|
||||
existingReversedTransactionIds);
|
||||
createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO);
|
||||
} else if (rescheduleBasedOnMeetingDates != null && rescheduleBasedOnMeetingDates) {
|
||||
@@ -2583,7 +2630,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
throw new PlatformServiceUnavailableException("error.msg.loan.status.not.written.off.update.not.allowed",
|
||||
"Loan :" + loanId + " update not allowed as loan status is not written off", loanId);
|
||||
}
|
||||
LocalDate recalculateFrom = null;
|
||||
LoanTransaction writeOffTransaction = loan.findWriteOffTransaction();
|
||||
if (writeOffTransaction == null) {
|
||||
throw new PlatformServiceUnavailableException("error.msg.loan.write.off.transaction.not.found",
|
||||
@@ -2591,10 +2637,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
}
|
||||
businessEventNotifierService.notifyPreBusinessEvent(new LoanUndoWrittenOffBusinessEvent(writeOffTransaction));
|
||||
|
||||
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
|
||||
|
||||
ChangedTransactionDetail changedTransactionDetail = loan.undoWrittenOff(loanLifecycleStateMachine, existingTransactionIds,
|
||||
existingReversedTransactionIds, scheduleGeneratorDTO);
|
||||
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.WRITE_OFF_OUTSTANDING_UNDO);
|
||||
ChangedTransactionDetail changedTransactionDetail = undoWrittenOff(loan, loanLifecycleStateMachine, existingTransactionIds,
|
||||
existingReversedTransactionIds);
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
|
||||
@@ -2698,7 +2743,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
|
||||
this.validateForAddAndDeleteTranche(loan);
|
||||
|
||||
loan.updateDisbursementDetails(command, actualChanges);
|
||||
loanDisbursementService.updateDisbursementDetails(loan, command, actualChanges);
|
||||
|
||||
if (loan.loanProduct().isDisallowExpectedDisbursements()) {
|
||||
if (!loan.getDisbursementDetails().isEmpty()) {
|
||||
@@ -2735,15 +2780,16 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
ChangedTransactionDetail changedTransactionDetail = null;
|
||||
|
||||
if (command.entityId() != null) {
|
||||
changedTransactionDetail = loan.updateDisbursementDateAndAmountForTranche(loanDisbursementDetails, command, changes,
|
||||
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_EDIT_MULTI_DISBURSE_DATE);
|
||||
changedTransactionDetail = updateDisbursementDateAndAmountForTranche(loan, loanDisbursementDetails, command, changes,
|
||||
scheduleGeneratorDTO);
|
||||
} else {
|
||||
loan.repaymentScheduleDetail().setPrincipal(loan.getPrincipalAmountForRepaymentSchedule());
|
||||
|
||||
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
|
||||
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
|
||||
} else {
|
||||
loan.regenerateRepaymentSchedule(scheduleGeneratorDTO);
|
||||
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
|
||||
loan.processPostDisbursementTransactions();
|
||||
}
|
||||
}
|
||||
@@ -2816,7 +2862,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
|
||||
ScheduleGeneratorDTO generatorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
|
||||
|
||||
ChangedTransactionDetail changedTransactionDetail = loan.recalculateScheduleFromLastTransaction(generatorDTO,
|
||||
ChangedTransactionDetail changedTransactionDetail = loanScheduleService.recalculateScheduleFromLastTransaction(loan, generatorDTO,
|
||||
existingTransactionIds, existingReversedTransactionIds);
|
||||
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
@@ -2908,9 +2954,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
if (loan.isActualDisbursedOnDateEarlierOrLaterThanExpected(actualDisbursementDate) || recalculateSchedule || isEmiAmountChanged
|
||||
|| rescheduledRepaymentDate != null) {
|
||||
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
|
||||
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
|
||||
} else {
|
||||
loan.regenerateRepaymentSchedule(scheduleGeneratorDTO);
|
||||
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
|
||||
}
|
||||
}
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
@@ -3089,8 +3135,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
|
||||
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFromDate);
|
||||
|
||||
final Map<String, Object> changes = loan.undoLastDisbursal(scheduleGeneratorDTO, existingTransactionIds,
|
||||
existingReversedTransactionIds, loan);
|
||||
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_DISBURSAL_UNDO_LAST);
|
||||
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_DISBURSAL_UNDO_LAST,
|
||||
loan.getDisbursementDate());
|
||||
final Map<String, Object> changes = undoLastDisbursal(scheduleGeneratorDTO, existingTransactionIds, existingReversedTransactionIds,
|
||||
loan);
|
||||
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
@@ -3268,6 +3317,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
final String reversalExternalId = command.stringValueOfParameterNamedAllowingNull(LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME);
|
||||
final ExternalId reversalTxnExternalId = ExternalIdFactory.produce(reversalExternalId);
|
||||
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(chargedOffTransaction.getLoan(), chargedOffTransaction,
|
||||
"reversed");
|
||||
chargedOffTransaction.reverse(reversalTxnExternalId);
|
||||
chargedOffTransaction.manuallyAdjustedOrReversed();
|
||||
|
||||
@@ -3367,6 +3418,21 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
.build();
|
||||
}
|
||||
|
||||
public void handleChargebackTransaction(final Loan loan, final LoanTransaction chargebackTransaction,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
|
||||
loanTransactionValidator.validateIfTransactionIsChargeback(chargebackTransaction);
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessor();
|
||||
|
||||
loan.addLoanTransaction(chargebackTransaction);
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargebackTransaction, new TransactionCtx(loan.getCurrency(),
|
||||
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
|
||||
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
if (!loan.doPostLoanTransactionChecks(chargebackTransaction.getTransactionDate(), loanLifecycleStateMachine)) {
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGEBACK, loan);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(Loan loan) {
|
||||
if (!loan.isMultiDisburmentLoan()) {
|
||||
final String errorMessage = "loan.product.does.not.support.multiple.disbursals.cannot.undo.last.disbursal";
|
||||
@@ -3400,4 +3466,422 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> undoDisbursal(final Loan loan, final ScheduleGeneratorDTO scheduleGeneratorDTO,
|
||||
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds) {
|
||||
final Map<String, Object> actualChanges = new LinkedHashMap<>();
|
||||
final LoanStatus currentStatus = loan.getStatus();
|
||||
final LoanStatus statusEnum = this.loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_DISBURSAL_UNDO, loan);
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
if (!statusEnum.hasStateOf(currentStatus)) {
|
||||
this.loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSAL_UNDO, loan);
|
||||
actualChanges.put(PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus()));
|
||||
|
||||
final LocalDate actualDisbursementDate = loan.getDisbursementDate();
|
||||
final boolean isScheduleRegenerateRequired = loan.isActualDisbursedOnDateEarlierOrLaterThanExpected(actualDisbursementDate);
|
||||
loan.setActualDisbursementDate(null);
|
||||
loan.setDisbursedBy(null);
|
||||
final boolean isDisbursedAmountChanged = !MathUtil.isEqualTo(loan.getApprovedPrincipal(),
|
||||
loan.getLoanRepaymentScheduleDetail().getPrincipal().getAmount());
|
||||
loan.getLoanRepaymentScheduleDetail().setPrincipal(loan.getApprovedPrincipal());
|
||||
// Remove All the Disbursement Details If the Loan Product is disabled and exists one
|
||||
if (loan.loanProduct().isDisallowExpectedDisbursements() && !loan.getDisbursementDetails().isEmpty()) {
|
||||
for (LoanDisbursementDetails disbursementDetail : loan.getAllDisbursementDetails()) {
|
||||
disbursementDetail.reverse();
|
||||
}
|
||||
} else {
|
||||
for (final LoanDisbursementDetails details : loan.getDisbursementDetails()) {
|
||||
details.updateActualDisbursementDate(null);
|
||||
}
|
||||
}
|
||||
final boolean isEmiAmountChanged = !loan.getLoanTermVariations().isEmpty();
|
||||
|
||||
updateLoanToPreDisbursalState(loan);
|
||||
if (isScheduleRegenerateRequired || isDisbursedAmountChanged || isEmiAmountChanged
|
||||
|| loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
// clear off actual disbusrement date so schedule regeneration
|
||||
// uses expected date.
|
||||
|
||||
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
|
||||
if (isDisbursedAmountChanged) {
|
||||
loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement());
|
||||
}
|
||||
} else if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
|
||||
for (final LoanRepaymentScheduleInstallment period : loan.getRepaymentScheduleInstallments()) {
|
||||
period.resetAccrualComponents();
|
||||
}
|
||||
}
|
||||
|
||||
if (loan.isTopup()) {
|
||||
loan.getLoanTopupDetails().setAccountTransferDetails(null);
|
||||
loan.getLoanTopupDetails().setTopupAmount(null);
|
||||
}
|
||||
|
||||
loan.adjustNetDisbursalAmount(loan.getApprovedPrincipal());
|
||||
actualChanges.put(ACTUAL_DISBURSEMENT_DATE, "");
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
}
|
||||
|
||||
return actualChanges;
|
||||
}
|
||||
|
||||
public void updateLoanToPreDisbursalState(final Loan loan) {
|
||||
loan.setActualDisbursementDate(null);
|
||||
|
||||
loan.setAccruedTill(null);
|
||||
reverseExistingTransactions(loan);
|
||||
|
||||
for (final LoanCharge charge : loan.getActiveCharges()) {
|
||||
if (charge.isOverdueInstallmentCharge()) {
|
||||
charge.setActive(false);
|
||||
} else {
|
||||
charge.resetToOriginal(loan.loanCurrency());
|
||||
}
|
||||
}
|
||||
final List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||
currentInstallment.resetDerivedComponents();
|
||||
}
|
||||
for (LoanTermVariations variations : loan.getLoanTermVariations()) {
|
||||
if (variations.getOnLoanStatus().equals(LoanStatus.ACTIVE.getValue())) {
|
||||
variations.markAsInactive();
|
||||
}
|
||||
}
|
||||
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
|
||||
wrapper.reprocess(loan.getCurrency(), loan.getDisbursementDate(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
|
||||
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
}
|
||||
|
||||
private void reverseExistingTransactions(final Loan loan) {
|
||||
final Collection<LoanTransaction> retainTransactions = new ArrayList<>();
|
||||
for (final LoanTransaction transaction : loan.getLoanTransactions()) {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(transaction.getLoan(), transaction, "reversed");
|
||||
transaction.reverse();
|
||||
if (transaction.getId() != null) {
|
||||
retainTransactions.add(transaction);
|
||||
}
|
||||
}
|
||||
loan.getLoanTransactions().retainAll(retainTransactions);
|
||||
}
|
||||
|
||||
private ChangedTransactionDetail closeAsWrittenOff(final Loan loan, final JsonCommand command,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map<String, Object> changes,
|
||||
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, final AppUser currentUser,
|
||||
final ScheduleGeneratorDTO scheduleGeneratorDTO) {
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessor();
|
||||
ChangedTransactionDetail changedTransactionDetail = closeDisbursements(loan, scheduleGeneratorDTO);
|
||||
|
||||
final LocalDate writtenOffOnLocalDate = command.localDateValueOfParameterNamed(TRANSACTION_DATE);
|
||||
loan.setClosedOnDate(writtenOffOnLocalDate);
|
||||
loan.setWrittenOffOnDate(writtenOffOnLocalDate);
|
||||
loan.setClosedBy(currentUser);
|
||||
final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.WRITE_OFF_OUTSTANDING, loan);
|
||||
|
||||
LoanTransaction loanTransaction = null;
|
||||
if (!statusEnum.hasStateOf(loan.getStatus())) {
|
||||
loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING, loan);
|
||||
changes.put(PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus()));
|
||||
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
|
||||
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull(EXTERNAL_ID);
|
||||
|
||||
ExternalId externalId = ExternalIdFactory.produce(txnExternalId);
|
||||
|
||||
if (externalId.isEmpty() && TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) {
|
||||
externalId = ExternalId.generate();
|
||||
}
|
||||
|
||||
changes.put(CLOSED_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE));
|
||||
changes.put(WRITTEN_OFF_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE));
|
||||
changes.put("externalId", externalId);
|
||||
|
||||
if (DateUtils.isBefore(writtenOffOnLocalDate, loan.getDisbursementDate())) {
|
||||
final String errorMessage = "The date on which a loan is written off cannot be before the loan disbursement date: "
|
||||
+ loan.getDisbursementDate().toString();
|
||||
throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.before.submittal.date", errorMessage,
|
||||
writtenOffOnLocalDate, loan.getDisbursementDate());
|
||||
}
|
||||
|
||||
if (DateUtils.isDateInTheFuture(writtenOffOnLocalDate)) {
|
||||
final String errorMessage = "The date on which a loan is written off cannot be in the future.";
|
||||
throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.a.future.date", errorMessage, writtenOffOnLocalDate);
|
||||
}
|
||||
|
||||
loanTransaction = LoanTransaction.writeoff(loan, loan.getOffice(), writtenOffOnLocalDate, externalId);
|
||||
LocalDate lastTransactionDate = loan.getLastUserTransactionDate();
|
||||
if (DateUtils.isAfter(lastTransactionDate, writtenOffOnLocalDate)) {
|
||||
final String errorMessage = "The date of the writeoff transaction must occur on or before previous transactions.";
|
||||
throw new InvalidLoanStateTransitionException("writeoff", "must.occur.on.or.after.other.transaction.dates", errorMessage,
|
||||
writtenOffOnLocalDate);
|
||||
}
|
||||
|
||||
loan.addLoanTransaction(loanTransaction);
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction,
|
||||
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
|
||||
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
|
||||
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
}
|
||||
if (changedTransactionDetail == null) {
|
||||
changedTransactionDetail = new ChangedTransactionDetail();
|
||||
}
|
||||
changedTransactionDetail.getNewTransactionMappings().put(0L, loanTransaction);
|
||||
return changedTransactionDetail;
|
||||
}
|
||||
|
||||
private ChangedTransactionDetail closeDisbursements(final Loan loan, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
|
||||
ChangedTransactionDetail changedTransactionDetail = null;
|
||||
if (loan.isDisbursementAllowed() && loan.atLeastOnceDisbursed()) {
|
||||
loan.getLoanRepaymentScheduleDetail().setPrincipal(loan.getDisbursedAmount());
|
||||
loan.removeDisbursementDetail();
|
||||
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
|
||||
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
|
||||
}
|
||||
changedTransactionDetail = loan.reprocessTransactions();
|
||||
LocalDate lastLoanTransactionDate = loan.getLatestTransactionDate();
|
||||
loan.doPostLoanTransactionChecks(lastLoanTransactionDate, loanLifecycleStateMachine);
|
||||
}
|
||||
return changedTransactionDetail;
|
||||
}
|
||||
|
||||
private ChangedTransactionDetail close(final Loan loan, final JsonCommand command,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map<String, Object> changes,
|
||||
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
|
||||
final ScheduleGeneratorDTO scheduleGeneratorDTO) {
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
|
||||
final LocalDate closureDate = command.localDateValueOfParameterNamed(TRANSACTION_DATE);
|
||||
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull(EXTERNAL_ID);
|
||||
|
||||
ExternalId externalId = ExternalIdFactory.produce(txnExternalId);
|
||||
if (externalId.isEmpty() && TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) {
|
||||
externalId = ExternalId.generate();
|
||||
}
|
||||
|
||||
loan.setClosedOnDate(closureDate);
|
||||
changes.put(CLOSED_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE));
|
||||
|
||||
if (DateUtils.isBefore(closureDate, loan.getDisbursementDate())) {
|
||||
final String errorMessage = "The date on which a loan is closed cannot be before the loan disbursement date: "
|
||||
+ loan.getDisbursementDate().toString();
|
||||
throw new InvalidLoanStateTransitionException("close", "cannot.be.before.submittal.date", errorMessage, closureDate,
|
||||
loan.getDisbursementDate());
|
||||
}
|
||||
|
||||
if (DateUtils.isDateInTheFuture(closureDate)) {
|
||||
final String errorMessage = "The date on which a loan is closed cannot be in the future.";
|
||||
throw new InvalidLoanStateTransitionException("close", "cannot.be.a.future.date", errorMessage, closureDate);
|
||||
}
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessor();
|
||||
ChangedTransactionDetail changedTransactionDetail = closeDisbursements(loan, scheduleGeneratorDTO);
|
||||
|
||||
LoanTransaction loanTransaction = null;
|
||||
if (loan.isOpen()) {
|
||||
final Money totalOutstanding = loan.getSummary().getTotalOutstanding(loan.getCurrency());
|
||||
if (totalOutstanding.isGreaterThanZero() && loan.getInArrearsTolerance().isGreaterThanOrEqualTo(totalOutstanding)) {
|
||||
|
||||
loan.setClosedOnDate(closureDate);
|
||||
final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.REPAID_IN_FULL, loan);
|
||||
if (!statusEnum.hasStateOf(loan.getStatus())) {
|
||||
loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, loan);
|
||||
changes.put(PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus()));
|
||||
}
|
||||
changes.put("externalId", externalId);
|
||||
loanTransaction = LoanTransaction.writeoff(loan, loan.getOffice(), closureDate, externalId);
|
||||
final boolean isLastTransaction = loan.isChronologicallyLatestTransaction(loanTransaction, loan.getLoanTransactions());
|
||||
if (!isLastTransaction) {
|
||||
final String errorMessage = "The closing date of the loan must be on or after latest transaction date.";
|
||||
throw new InvalidLoanStateTransitionException("close.loan", "must.occur.on.or.after.latest.transaction.date",
|
||||
errorMessage, closureDate);
|
||||
}
|
||||
|
||||
loan.addLoanTransaction(loanTransaction);
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction,
|
||||
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
|
||||
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
|
||||
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
} else if (totalOutstanding.isGreaterThanZero()) {
|
||||
final String errorMessage = "A loan with money outstanding cannot be closed";
|
||||
throw new InvalidLoanStateTransitionException("close", "loan.has.money.outstanding", errorMessage,
|
||||
totalOutstanding.toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (loan.isOverPaid()) {
|
||||
final Money totalLoanOverpayment = loan.calculateTotalOverpayment();
|
||||
if (totalLoanOverpayment.isGreaterThanZero() && loan.getInArrearsTolerance().isGreaterThanOrEqualTo(totalLoanOverpayment)) {
|
||||
// TODO - KW - technically should set somewhere that this loan
|
||||
// has 'overpaid' amount
|
||||
loan.setClosedOnDate(closureDate);
|
||||
final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.REPAID_IN_FULL, loan);
|
||||
if (!statusEnum.hasStateOf(loan.getStatus())) {
|
||||
loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, loan);
|
||||
changes.put(PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus()));
|
||||
}
|
||||
} else if (totalLoanOverpayment.isGreaterThanZero()) {
|
||||
final String errorMessage = "The loan is marked as 'Overpaid' and cannot be moved to 'Closed (obligations met).";
|
||||
throw new InvalidLoanStateTransitionException("close", "loan.is.overpaid", errorMessage, totalLoanOverpayment.toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (changedTransactionDetail == null) {
|
||||
changedTransactionDetail = new ChangedTransactionDetail();
|
||||
}
|
||||
changedTransactionDetail.getNewTransactionMappings().put(0L, loanTransaction);
|
||||
return changedTransactionDetail;
|
||||
}
|
||||
|
||||
private ChangedTransactionDetail updateDisbursementDateAndAmountForTranche(final Loan loan,
|
||||
final LoanDisbursementDetails disbursementDetails, final JsonCommand command, final Map<String, Object> actualChanges,
|
||||
final ScheduleGeneratorDTO scheduleGeneratorDTO) {
|
||||
final Locale locale = command.extractLocale();
|
||||
final BigDecimal principal = command.bigDecimalValueOfParameterNamed(LoanApiConstants.updatedDisbursementPrincipalParameterName,
|
||||
locale);
|
||||
final LocalDate expectedDisbursementDate = command
|
||||
.localDateValueOfParameterNamed(LoanApiConstants.updatedDisbursementDateParameterName);
|
||||
disbursementDetails.updateExpectedDisbursementDateAndAmount(expectedDisbursementDate, principal);
|
||||
actualChanges.put(LoanApiConstants.expectedDisbursementDateParameterName,
|
||||
command.stringValueOfParameterNamed(LoanApiConstants.expectedDisbursementDateParameterName));
|
||||
actualChanges.put(LoanApiConstants.disbursementIdParameterName,
|
||||
command.stringValueOfParameterNamed(LoanApiConstants.disbursementIdParameterName));
|
||||
actualChanges.put(LoanApiConstants.disbursementPrincipalParameterName,
|
||||
command.bigDecimalValueOfParameterNamed(LoanApiConstants.disbursementPrincipalParameterName, locale));
|
||||
|
||||
loan.getLoanRepaymentScheduleDetail().setPrincipal(loan.getPrincipalAmountForRepaymentSchedule());
|
||||
|
||||
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
|
||||
} else {
|
||||
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
|
||||
}
|
||||
|
||||
return loan.reprocessTransactions();
|
||||
}
|
||||
|
||||
private Map<String, Object> undoLastDisbursal(final ScheduleGeneratorDTO scheduleGeneratorDTO, final List<Long> existingTransactionIds,
|
||||
final List<Long> existingReversedTransactionIds, final Loan loan) {
|
||||
final Map<String, Object> actualChanges = new LinkedHashMap<>();
|
||||
List<LoanTransaction> loanTransactions = loan.retrieveListOfTransactionsByType(LoanTransactionType.DISBURSEMENT);
|
||||
loanTransactions.sort(Comparator.comparing(LoanTransaction::getId));
|
||||
final LoanTransaction lastDisbursalTransaction = loanTransactions.get(loanTransactions.size() - 1);
|
||||
final LocalDate lastTransactionDate = lastDisbursalTransaction.getTransactionDate();
|
||||
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
|
||||
loanTransactions = loan.retrieveListOfTransactionsExcludeAccruals();
|
||||
Collections.reverse(loanTransactions);
|
||||
for (final LoanTransaction previousTransaction : loanTransactions) {
|
||||
if (DateUtils.isBefore(lastTransactionDate, previousTransaction.getTransactionDate())
|
||||
&& (previousTransaction.isRepaymentLikeType() || previousTransaction.isWaiver()
|
||||
|| previousTransaction.isChargePayment())) {
|
||||
throw new UndoLastTrancheDisbursementException(previousTransaction.getId());
|
||||
}
|
||||
if (previousTransaction.getId().compareTo(lastDisbursalTransaction.getId()) < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
final LoanDisbursementDetails disbursementDetail = loan.getDisbursementDetails(lastTransactionDate,
|
||||
lastDisbursalTransaction.getAmount());
|
||||
loan.updateLoanToLastDisbursalState(disbursementDetail);
|
||||
loan.getLoanTermVariations()
|
||||
.removeIf(loanTermVariations -> (loanTermVariations.getTermType().isDueDateVariation()
|
||||
&& DateUtils.isAfter(loanTermVariations.fetchDateValue(), lastTransactionDate))
|
||||
|| (loanTermVariations.getTermType().isEMIAmountVariation()
|
||||
&& DateUtils.isEqual(loanTermVariations.getTermApplicableFrom(), lastTransactionDate))
|
||||
|| DateUtils.isAfter(loanTermVariations.getTermApplicableFrom(), lastTransactionDate));
|
||||
reverseExistingTransactionsTillLastDisbursal(loan, lastDisbursalTransaction);
|
||||
loanScheduleService.recalculateScheduleFromLastTransaction(loan, scheduleGeneratorDTO);
|
||||
actualChanges.put("undolastdisbursal", "true");
|
||||
actualChanges.put("disbursedAmount", loan.getDisbursedAmount());
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
|
||||
loan.doPostLoanTransactionChecks(loan.getLastUserTransactionDate(), loanLifecycleStateMachine);
|
||||
|
||||
return actualChanges;
|
||||
}
|
||||
|
||||
private ChangedTransactionDetail waiveInterest(final Loan loan, final LoanTransaction waiveInterestTransaction,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
|
||||
final List<Long> existingReversedTransactionIds, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
|
||||
return loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, waiveInterestTransaction,
|
||||
loanLifecycleStateMachine, null, scheduleGeneratorDTO);
|
||||
}
|
||||
|
||||
private ChangedTransactionDetail undoWrittenOff(final Loan loan, final LoanLifecycleStateMachine loanLifecycleStateMachine,
|
||||
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds) {
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
final LoanTransaction writeOffTransaction = loan.findWriteOffTransaction();
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(writeOffTransaction.getLoan(), writeOffTransaction,
|
||||
"reversed");
|
||||
writeOffTransaction.reverse();
|
||||
loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO, loan);
|
||||
return loan.reprocessTransactions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse only disbursement, accruals, and repayments at disbursal transactions
|
||||
*/
|
||||
public void reverseExistingTransactionsTillLastDisbursal(final Loan loan, final LoanTransaction lastDisbursalTransaction) {
|
||||
for (final LoanTransaction transaction : loan.getLoanTransactions()) {
|
||||
if (!DateUtils.isBefore(transaction.getTransactionDate(), lastDisbursalTransaction.getTransactionDate())
|
||||
&& transaction.getId().compareTo(lastDisbursalTransaction.getId()) >= 0
|
||||
&& transaction.isAllowTypeTransactionAtTheTimeOfLastUndo()) {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(transaction.getLoan(), transaction, "reversed");
|
||||
transaction.reverse();
|
||||
}
|
||||
}
|
||||
if (loan.isAutoRepaymentForDownPaymentEnabled()) {
|
||||
// identify down-payment amount for the transaction
|
||||
BigDecimal disbursedAmountPercentageForDownPayment = loan.getLoanRepaymentScheduleDetail()
|
||||
.getDisbursedAmountPercentageForDownPayment();
|
||||
Money downPaymentMoney = Money.of(loan.getCurrency(),
|
||||
MathUtil.percentageOf(lastDisbursalTransaction.getAmount(), disbursedAmountPercentageForDownPayment, 19));
|
||||
|
||||
// find the latest matching down-payment transaction based on date, amount and transaction type
|
||||
Optional<LoanTransaction> downPaymentTransaction = loan.getLoanTransactions().stream()
|
||||
.filter(tr -> tr.getTransactionDate().equals(lastDisbursalTransaction.getTransactionDate())
|
||||
&& tr.getTypeOf().isDownPayment() && tr.getAmount().compareTo(downPaymentMoney.getAmount()) == 0)
|
||||
.max(Comparator.comparing(LoanTransaction::getId));
|
||||
|
||||
// reverse the down-payment transaction
|
||||
downPaymentTransaction.ifPresent(tr -> {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(tr.getLoan(), tr, "reversed");
|
||||
tr.reverse();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Behaviour added to comply with capability of previous mifos product to support easier transition to fineract
|
||||
* platform.
|
||||
*/
|
||||
public void closeAsMarkedForReschedule(final Loan loan, final JsonCommand command,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map<String, Object> changes) {
|
||||
final LocalDate rescheduledOn = command.localDateValueOfParameterNamed(TRANSACTION_DATE);
|
||||
|
||||
loan.setClosedOnDate(rescheduledOn);
|
||||
final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_RESCHEDULE, loan);
|
||||
if (!statusEnum.hasStateOf(loan.getStatus())) {
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_RESCHEDULE, loan);
|
||||
changes.put(PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus()));
|
||||
}
|
||||
|
||||
loan.setRescheduledOnDate(rescheduledOn);
|
||||
changes.put(CLOSED_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE));
|
||||
changes.put("rescheduledOnDate", command.stringValueOfParameterNamed(TRANSACTION_DATE));
|
||||
|
||||
loanTransactionValidator.validateLoanRescheduleDate(loan);
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -51,6 +51,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
|
||||
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.LoanTransactionNotFoundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||
import org.apache.fineract.portfolio.note.domain.Note;
|
||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||
@@ -70,6 +71,7 @@ public class LoanReAgingServiceImpl {
|
||||
private final LoanReAgingParameterRepository reAgingParameterRepository;
|
||||
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
|
||||
private final NoteRepository noteRepository;
|
||||
private final LoanChargeValidator loanChargeValidator;
|
||||
|
||||
public CommandProcessingResult reAge(Long loanId, JsonCommand command) {
|
||||
Loan loan = loanAssembler.assembleFrom(loanId);
|
||||
@@ -141,6 +143,8 @@ public class LoanReAgingServiceImpl {
|
||||
|
||||
private void reverseReAgeTransaction(LoanTransaction reAgeTransaction, JsonCommand command) {
|
||||
ExternalId reversalExternalId = externalIdFactory.createFromCommand(command, LoanReAgingApiConstants.externalIdParameterName);
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(reAgeTransaction.getLoan(), reAgeTransaction,
|
||||
"reversed");
|
||||
reAgeTransaction.reverse(reversalExternalId);
|
||||
reAgeTransaction.manuallyAdjustedOrReversed();
|
||||
}
|
||||
|
||||
+4
@@ -47,6 +47,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
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.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -62,6 +63,7 @@ public class LoanReAmortizationServiceImpl {
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final LoanTransactionRepository loanTransactionRepository;
|
||||
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
|
||||
private final LoanChargeValidator loanChargeValidator;
|
||||
|
||||
public CommandProcessingResult reAmortize(Long loanId, JsonCommand command) {
|
||||
Loan loan = loanAssembler.assembleFrom(loanId);
|
||||
@@ -126,6 +128,8 @@ public class LoanReAmortizationServiceImpl {
|
||||
private void reverseReAmortizeTransaction(LoanTransaction reAmortizeTransaction, JsonCommand command) {
|
||||
ExternalId reversalExternalId = externalIdFactory.createFromCommand(command,
|
||||
LoanReAmortizationApiConstants.externalIdParameterName);
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(reAmortizeTransaction.getLoan(), reAmortizeTransaction,
|
||||
"reversed");
|
||||
reAmortizeTransaction.reverse(reversalExternalId);
|
||||
reAmortizeTransaction.manuallyAdjustedOrReversed();
|
||||
reAmortizeTransaction.getLoan().reprocessTransactions();
|
||||
|
||||
+68
-15
@@ -85,6 +85,12 @@ import org.apache.fineract.portfolio.loanaccount.mapper.LoanCollateralManagement
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanDisbursementValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanForeclosureValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanOfficerValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanRefundValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.BulkLoansReadPlatformService;
|
||||
@@ -108,13 +114,18 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargePaidByReadService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformServiceImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformServiceImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementDetailsAssembler;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerServiceImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanRefundService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanStatusChangePlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanStatusChangePlatformServiceImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionAssembler;
|
||||
@@ -186,13 +197,15 @@ public class LoanAccountConfiguration {
|
||||
LoanUtilService loanUtilService, CalendarReadPlatformService calendarReadPlatformService,
|
||||
EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, GLIMAccountInfoRepository glimRepository,
|
||||
LoanRepository loanRepository, GSIMReadPlatformService gsimReadPlatformService,
|
||||
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanAccrualsProcessingService loanAccrualsProcessingService) {
|
||||
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanAccrualsProcessingService loanAccrualsProcessingService,
|
||||
LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanScheduleService loanScheduleService) {
|
||||
return new LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, loanApplicationTransitionValidator,
|
||||
loanApplicationValidator, loanRepositoryWrapper, noteRepository, loanAssembler, loanSummaryWrapper,
|
||||
loanRepaymentScheduleTransactionProcessorFactory, calendarRepository, calendarInstanceRepository, savingsAccountRepository,
|
||||
accountAssociationsRepository, businessEventNotifierService, loanScheduleAssembler, loanUtilService,
|
||||
calendarReadPlatformService, entityDatatableChecksWritePlatformService, glimRepository, loanRepository,
|
||||
gsimReadPlatformService, defaultLoanLifecycleStateMachine, loanAccrualsProcessingService);
|
||||
gsimReadPlatformService, defaultLoanLifecycleStateMachine, loanAccrualsProcessingService,
|
||||
loanDownPaymentTransactionValidator, loanScheduleService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -217,14 +230,15 @@ public class LoanAccountConfiguration {
|
||||
AccountNumberGenerator accountNumberGenerator, GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService,
|
||||
LoanCollateralAssembler loanCollateralAssembler, LoanScheduleCalculationPlatformService calculationPlatformService,
|
||||
LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler, LoanChargeMapper loanChargeMapper,
|
||||
LoanCollateralManagementMapper loanCollateralManagementMapper, LoanAccrualsProcessingService loanAccrualsProcessingService) {
|
||||
LoanCollateralManagementMapper loanCollateralManagementMapper, LoanAccrualsProcessingService loanAccrualsProcessingService,
|
||||
LoanDisbursementService loanDisbursementService, LoanChargeService loanChargeService, LoanOfficerService loanOfficerService) {
|
||||
return new LoanAssemblerImpl(fromApiJsonHelper, loanRepository, loanProductRepository, clientRepository, groupRepository,
|
||||
fundRepository, staffRepository, codeValueRepository, loanScheduleAssembler, loanChargeAssembler, collateralAssembler,
|
||||
loanSummaryWrapper, loanRepaymentScheduleTransactionProcessorFactory, holidayRepository, configurationDomainService,
|
||||
workingDaysRepository, rateAssembler, defaultLoanLifecycleStateMachine, externalIdFactory, accountNumberFormatRepository,
|
||||
glimRepository, accountNumberGenerator, glimAccountInfoWritePlatformService, loanCollateralAssembler,
|
||||
calculationPlatformService, loanDisbursementDetailsAssembler, loanChargeMapper, loanCollateralManagementMapper,
|
||||
loanAccrualsProcessingService);
|
||||
loanAccrualsProcessingService, loanDisbursementService, loanChargeService, loanOfficerService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -275,9 +289,9 @@ public class LoanAccountConfiguration {
|
||||
LoanChargeAssembler loanChargeAssembler, ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService,
|
||||
PaymentDetailWritePlatformService paymentDetailWritePlatformService, NoteRepository noteRepository,
|
||||
LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService,
|
||||
LoanAccrualsProcessingService loanAccrualsProcessingService
|
||||
|
||||
) {
|
||||
LoanAccrualsProcessingService loanAccrualsProcessingService,
|
||||
LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanChargeValidator loanChargeValidator,
|
||||
LoanScheduleService loanScheduleService) {
|
||||
return new LoanChargeWritePlatformServiceImpl(loanChargeApiJsonValidator, loanAssembler, chargeRepository,
|
||||
businessEventNotifierService, loanTransactionRepository, accountTransfersWritePlatformService, loanRepositoryWrapper,
|
||||
journalEntryWritePlatformService, loanAccountDomainService, loanChargeRepository, loanWritePlatformService, loanUtilService,
|
||||
@@ -285,7 +299,7 @@ public class LoanAccountConfiguration {
|
||||
configurationDomainService, loanRepaymentScheduleTransactionProcessorFactory, externalIdFactory,
|
||||
accountTransferDetailRepository, loanChargeAssembler, replayedTransactionBusinessEventService,
|
||||
paymentDetailWritePlatformService, noteRepository, loanAccrualTransactionBusinessEventService,
|
||||
loanAccrualsProcessingService);
|
||||
loanAccrualsProcessingService, loanDownPaymentTransactionValidator, loanChargeValidator, loanScheduleService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -303,15 +317,16 @@ public class LoanAccountConfiguration {
|
||||
ConfigurationDomainService configurationDomainService, AccountDetailsReadPlatformService accountDetailsReadPlatformService,
|
||||
ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator,
|
||||
DelinquencyReadPlatformService delinquencyReadPlatformService, LoanTransactionRepository loanTransactionRepository,
|
||||
LoanChargePaidByReadService loanChargePaidByReadService,
|
||||
LoanTransactionRelationReadService loanTransactionRelationReadService) {
|
||||
LoanChargePaidByReadService loanChargePaidByReadService, LoanTransactionRelationReadService loanTransactionRelationReadService,
|
||||
LoanForeclosureValidator loanForeclosureValidator) {
|
||||
return new LoanReadPlatformServiceImpl(jdbcTemplate, context, loanRepositoryWrapper, applicationCurrencyRepository,
|
||||
loanProductReadPlatformService, clientReadPlatformService, groupReadPlatformService, loanDropdownReadPlatformService,
|
||||
fundReadPlatformService, chargeReadPlatformService, codeValueReadPlatformService, calendarReadPlatformService,
|
||||
staffReadPlatformService, paginationHelper, namedParameterJdbcTemplate, paymentTypeReadPlatformService,
|
||||
loanRepaymentScheduleTransactionProcessorFactory, floatingRatesReadPlatformService, loanUtilService,
|
||||
configurationDomainService, accountDetailsReadPlatformService, columnValidator, sqlGenerator,
|
||||
delinquencyReadPlatformService, loanTransactionRepository, loanChargePaidByReadService, loanTransactionRelationReadService);
|
||||
delinquencyReadPlatformService, loanTransactionRepository, loanChargePaidByReadService, loanTransactionRelationReadService,
|
||||
loanForeclosureValidator);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -362,7 +377,10 @@ public class LoanAccountConfiguration {
|
||||
ExternalIdFactory externalIdFactory, ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService,
|
||||
LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService, ErrorHandler errorHandler,
|
||||
LoanDownPaymentHandlerService loanDownPaymentHandlerService, AccountTransferRepository accountTransferRepository,
|
||||
LoanTransactionAssembler loanTransactionAssembler, LoanAccrualsProcessingService loanAccrualsProcessingService) {
|
||||
LoanTransactionAssembler loanTransactionAssembler, LoanAccrualsProcessingService loanAccrualsProcessingService,
|
||||
LoanOfficerValidator loanOfficerValidator, LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator,
|
||||
LoanDisbursementService loanDisbursementService, LoanScheduleService loanScheduleService,
|
||||
LoanChargeValidator loanChargeValidator, LoanOfficerService loanOfficerService) {
|
||||
return new LoanWritePlatformServiceJpaRepositoryImpl(transactionProcessorFactory, context, loanTransactionValidator,
|
||||
loanUpdateCommandFromApiJsonDeserializer, loanRepositoryWrapper, loanAccountDomainService, noteRepository,
|
||||
loanTransactionRepository, loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService,
|
||||
@@ -376,7 +394,8 @@ public class LoanAccountConfiguration {
|
||||
postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository, defaultLoanLifecycleStateMachine,
|
||||
loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService,
|
||||
loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService, accountTransferRepository,
|
||||
loanTransactionAssembler, loanAccrualsProcessingService);
|
||||
loanTransactionAssembler, loanAccrualsProcessingService, loanOfficerValidator, loanDownPaymentTransactionValidator,
|
||||
loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -389,8 +408,11 @@ public class LoanAccountConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(LoanDownPaymentHandlerService.class)
|
||||
public LoanDownPaymentHandlerService loanDownPaymentHandlerService(LoanTransactionRepository loanTransactionRepository,
|
||||
BusinessEventNotifierService businessEventNotifierService) {
|
||||
return new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService);
|
||||
BusinessEventNotifierService businessEventNotifierService,
|
||||
LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanScheduleService loanScheduleService,
|
||||
LoanRefundService loanRefundService, LoanRefundValidator loanRefundValidator) {
|
||||
return new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService,
|
||||
loanDownPaymentTransactionValidator, loanScheduleService, loanRefundService, loanRefundValidator);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -398,4 +420,35 @@ public class LoanAccountConfiguration {
|
||||
public LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler(FromJsonHelper fromApiJsonHelper) {
|
||||
return new LoanDisbursementDetailsAssembler(fromApiJsonHelper);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(LoanDisbursementService.class)
|
||||
public LoanDisbursementService loanDisbursementService(LoanChargeValidator loanChargeValidator,
|
||||
LoanDisbursementValidator loanDisbursementValidator) {
|
||||
return new LoanDisbursementService(loanChargeValidator, loanDisbursementValidator);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(LoanChargeService.class)
|
||||
public LoanChargeService loanChargeService(LoanChargeValidator loanChargeValidator) {
|
||||
return new LoanChargeService(loanChargeValidator);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(LoanScheduleService.class)
|
||||
public LoanScheduleService loanScheduleService(LoanChargeService loanChargeService) {
|
||||
return new LoanScheduleService(loanChargeService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(LoanOfficerService.class)
|
||||
public LoanOfficerService loanOfficerService(LoanOfficerValidator loanOfficerValidator) {
|
||||
return new LoanOfficerService(loanOfficerValidator);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(LoanRefundService.class)
|
||||
public LoanRefundService loanRefundService(LoanRefundValidator loanRefundValidator) {
|
||||
return new LoanRefundService(loanRefundValidator);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -52,6 +52,7 @@ import org.apache.fineract.portfolio.group.exception.ClientNotInGroupException;
|
||||
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
|
||||
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
@@ -81,6 +82,7 @@ public class TransferWritePlatformServiceJpaRepositoryImpl implements TransferWr
|
||||
private final StaffRepositoryWrapper staffRepositoryWrapper;
|
||||
private final ClientTransferDetailsRepositoryWrapper clientTransferDetailsRepositoryWrapper;
|
||||
private final PlatformSecurityContext context;
|
||||
private final LoanOfficerService loanOfficerService;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@@ -216,9 +218,9 @@ public class TransferWritePlatformServiceJpaRepositoryImpl implements TransferWr
|
||||
loan.updateGroup(destinationGroup);
|
||||
if (inheritDestinationGroupLoanOfficer != null && inheritDestinationGroupLoanOfficer == true
|
||||
&& destinationGroupLoanOfficer != null) {
|
||||
loan.reassignLoanOfficer(destinationGroupLoanOfficer, DateUtils.getBusinessLocalDate());
|
||||
loanOfficerService.reassignLoanOfficer(loan, destinationGroupLoanOfficer, DateUtils.getBusinessLocalDate());
|
||||
} else if (newLoanOfficer != null) {
|
||||
loan.reassignLoanOfficer(newLoanOfficer, DateUtils.getBusinessLocalDate());
|
||||
loanOfficerService.reassignLoanOfficer(loan, newLoanOfficer, DateUtils.getBusinessLocalDate());
|
||||
}
|
||||
this.loanRepositoryWrapper.saveAndFlush(loan);
|
||||
}
|
||||
|
||||
+4
-2
@@ -26,6 +26,7 @@ import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientTransferDetailsRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
|
||||
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
|
||||
@@ -49,10 +50,11 @@ public class TransferConfiguration {
|
||||
NoteWritePlatformService noteWritePlatformService, StaffRepositoryWrapper staffRepositoryWrapper,
|
||||
SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper,
|
||||
SavingsAccountWritePlatformService savingsAccountWritePlatformService,
|
||||
ClientTransferDetailsRepositoryWrapper clientTransferDetailsRepositoryWrapper, PlatformSecurityContext context) {
|
||||
ClientTransferDetailsRepositoryWrapper clientTransferDetailsRepositoryWrapper, PlatformSecurityContext context,
|
||||
LoanOfficerService loanOfficerService) {
|
||||
return new TransferWritePlatformServiceJpaRepositoryImpl(clientRepositoryWrapper, officeRepository, calendarInstanceRepository,
|
||||
groupRepository, loanWritePlatformService, savingsAccountWritePlatformService, loanRepositoryWrapper,
|
||||
savingsAccountRepositoryWrapper, transfersDataValidator, noteWritePlatformService, staffRepositoryWrapper,
|
||||
clientTransferDetailsRepositoryWrapper, context);
|
||||
clientTransferDetailsRepositoryWrapper, context, loanOfficerService);
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -46,6 +46,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -124,6 +125,9 @@ class LoanChargeWritePlatformServiceImplTest {
|
||||
@Mock
|
||||
private JournalEntryWritePlatformService journalEntryWritePlatformService;
|
||||
|
||||
@Mock
|
||||
private LoanChargeValidator loanChargeValidator;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(loanAssembler.assembleFrom(LOAN_ID)).thenReturn(loan);
|
||||
|
||||
+121
-24
@@ -21,24 +21,38 @@ package org.apache.fineract.portfolio.loanaccount.service;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
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.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||
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.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
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;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -54,7 +68,11 @@ import org.mockito.quality.Strictness;
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
public class LoanDownPaymentHandlerServiceImplTest {
|
||||
|
||||
private final MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
|
||||
private final MockedStatic<MoneyHelper> moneyHelper = mockStatic(MoneyHelper.class);
|
||||
private final MockedStatic<MathUtil> mathUtilMock = mockStatic(MathUtil.class);
|
||||
private final MockedStatic<DateUtils> dateUtilsMock = mockStatic(DateUtils.class);
|
||||
private final MockedStatic<TemporaryConfigurationServiceContainer> tempConfigServiceMock = mockStatic(
|
||||
TemporaryConfigurationServiceContainer.class);
|
||||
|
||||
@Mock
|
||||
private BusinessEventNotifierService businessEventNotifierService;
|
||||
@@ -66,42 +84,95 @@ public class LoanDownPaymentHandlerServiceImplTest {
|
||||
private LoanTransaction loanTransaction;
|
||||
|
||||
@Mock
|
||||
private ScheduleGeneratorDTO scheduleGeneratorDTO;
|
||||
private JsonCommand command;
|
||||
|
||||
@Mock
|
||||
private JsonCommand command;
|
||||
private LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator;
|
||||
|
||||
@Mock
|
||||
private LoanScheduleService loanScheduleService;
|
||||
|
||||
@Mock
|
||||
private LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor;
|
||||
|
||||
@Mock
|
||||
private LoanRefundService loanRefundService;
|
||||
|
||||
@Mock
|
||||
private LoanRefundValidator loanRefundValidator;
|
||||
|
||||
private LoanDownPaymentHandlerServiceImpl underTest;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
underTest = new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService);
|
||||
underTest = new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService,
|
||||
loanDownPaymentTransactionValidator, loanScheduleService, loanRefundService, loanRefundValidator);
|
||||
moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP));
|
||||
moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP);
|
||||
tempConfigServiceMock.when(TemporaryConfigurationServiceContainer::isExternalIdAutoGenerationEnabled).thenReturn(true);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void reset() {
|
||||
moneyHelper.close();
|
||||
tempConfigServiceMock.close();
|
||||
mathUtilMock.close();
|
||||
dateUtilsMock.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownPaymentHandler() {
|
||||
// given
|
||||
Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||
LoanTransaction disbursement = Mockito.mock(LoanTransaction.class);
|
||||
MonetaryCurrency loanCurrency = Mockito.mock(MonetaryCurrency.class);
|
||||
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||
final LoanTransaction disbursement = Mockito.mock(LoanTransaction.class);
|
||||
final Money mockAdjustedDownPaymentMoney = Mockito.mock(Money.class);
|
||||
final LoanProductRelatedDetail loanRepaymentRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class);
|
||||
final LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||
final LoanProductRelatedDetail loanProductRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class);
|
||||
final ScheduleGeneratorDTO scheduleGeneratorDTO = Mockito.mock(ScheduleGeneratorDTO.class);
|
||||
final HolidayDetailDTO holidayDetailDTO = Mockito.mock(HolidayDetailDTO.class);
|
||||
final ChangedTransactionDetail changedTransactionDetail = Mockito.mock(ChangedTransactionDetail.class);
|
||||
|
||||
final MonetaryCurrency loanCurrency = new MonetaryCurrency("USD", 2, 1);
|
||||
|
||||
final Money downPaymentMoney = Mockito.mock(Money.class);
|
||||
final Money overPaymentPortionMoney = Mockito.mock(Money.class);
|
||||
final Money calculatedMoney = Money.of(loanCurrency, BigDecimal.valueOf(500));
|
||||
|
||||
when(downPaymentMoney.getCurrencyCode()).thenReturn(loanCurrency.getCode());
|
||||
when(overPaymentPortionMoney.getCurrencyCode()).thenReturn(loanCurrency.getCode());
|
||||
|
||||
when(loanForProcessing.getLoanRepaymentScheduleDetail()).thenReturn(loanRepaymentRelatedDetail);
|
||||
when(loanForProcessing.repaymentScheduleDetail()).thenReturn(loanRepaymentRelatedDetail);
|
||||
when(loanRepaymentRelatedDetail.isInterestRecalculationEnabled()).thenReturn(true);
|
||||
when(loanRepaymentRelatedDetail.getDisbursedAmountPercentageForDownPayment()).thenReturn(BigDecimal.valueOf(10));
|
||||
when(loanForProcessing.getCurrency()).thenReturn(loanCurrency);
|
||||
when(loanForProcessing.loanCurrency()).thenReturn(loanCurrency);
|
||||
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
|
||||
when(loanProduct.getInstallmentAmountInMultiplesOf()).thenReturn(10);
|
||||
when(loanForProcessing.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
|
||||
when(loanForProcessing.getTransactionProcessor()).thenReturn(loanRepaymentScheduleTransactionProcessor);
|
||||
when(loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(any(), any(), any(), any(), any()))
|
||||
.thenReturn(changedTransactionDetail);
|
||||
when(loanProductRelatedDetail.getLoanScheduleType()).thenReturn(LoanScheduleType.PROGRESSIVE);
|
||||
|
||||
when(disbursement.getOverPaymentPortion(loanCurrency)).thenReturn(overPaymentPortionMoney);
|
||||
|
||||
when(downPaymentMoney.minus(overPaymentPortionMoney)).thenReturn(calculatedMoney);
|
||||
mathUtilMock.when(() -> MathUtil.negativeToZero(any(Money.class))).thenReturn(mockAdjustedDownPaymentMoney);
|
||||
when(mockAdjustedDownPaymentMoney.isGreaterThanZero()).thenReturn(true);
|
||||
|
||||
dateUtilsMock.when(DateUtils::getBusinessLocalDate).thenReturn(LocalDate.of(2024, 11, 19));
|
||||
|
||||
when(scheduleGeneratorDTO.getHolidayDetailDTO()).thenReturn(holidayDetailDTO);
|
||||
|
||||
doNothing().when(businessEventNotifierService).notifyPreBusinessEvent(any(LoanTransactionDownPaymentPreBusinessEvent.class));
|
||||
doNothing().when(businessEventNotifierService).notifyPostBusinessEvent(any(LoanTransactionDownPaymentPostBusinessEvent.class));
|
||||
doNothing().when(businessEventNotifierService).notifyPostBusinessEvent(any(LoanBalanceChangedBusinessEvent.class));
|
||||
when(loanTransactionRepository.saveAndFlush(any(LoanTransaction.class))).thenReturn(loanTransaction);
|
||||
when(loanForProcessing.handleDownPayment(eq(disbursement), eq(command), eq(scheduleGeneratorDTO))).thenReturn(loanTransaction);
|
||||
when(loanForProcessing.getCurrency()).thenReturn(loanCurrency);
|
||||
when(loanCurrency.getCode()).thenReturn("CODE");
|
||||
when(loanCurrency.getCurrencyInMultiplesOf()).thenReturn(1);
|
||||
when(loanCurrency.getDigitsAfterDecimal()).thenReturn(1);
|
||||
|
||||
// when
|
||||
LoanTransaction actual = underTest.handleDownPayment(scheduleGeneratorDTO, command, disbursement, loanForProcessing);
|
||||
final LoanTransaction actual = underTest.handleDownPayment(scheduleGeneratorDTO, command, disbursement, loanForProcessing);
|
||||
|
||||
// then
|
||||
assertNotNull(actual);
|
||||
@@ -110,21 +181,48 @@ public class LoanDownPaymentHandlerServiceImplTest {
|
||||
verify(businessEventNotifierService, Mockito.times(1))
|
||||
.notifyPostBusinessEvent(Mockito.any(LoanTransactionDownPaymentPostBusinessEvent.class));
|
||||
verify(businessEventNotifierService, Mockito.times(1)).notifyPostBusinessEvent(Mockito.any(LoanBalanceChangedBusinessEvent.class));
|
||||
verify(loanForProcessing, Mockito.times(1)).handleDownPayment(eq(disbursement), eq(command), eq(scheduleGeneratorDTO));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownPaymentHandlerNoNewTransaction() {
|
||||
// given
|
||||
Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||
LoanTransaction disbursement = Mockito.mock(LoanTransaction.class);
|
||||
MonetaryCurrency loanCurrency = Mockito.mock(MonetaryCurrency.class);
|
||||
doNothing().when(businessEventNotifierService).notifyPreBusinessEvent(any(LoanTransactionDownPaymentPreBusinessEvent.class));
|
||||
when(loanForProcessing.handleDownPayment(eq(disbursement), eq(command), eq(scheduleGeneratorDTO))).thenReturn(null);
|
||||
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||
final LoanTransaction disbursement = Mockito.mock(LoanTransaction.class);
|
||||
final Money mockAdjustedDownPaymentMoney = Mockito.mock(Money.class);
|
||||
final LoanProductRelatedDetail loanRepaymentRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class);
|
||||
final LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||
final LoanProductRelatedDetail loanProductRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class);
|
||||
final ScheduleGeneratorDTO scheduleGeneratorDTO = Mockito.mock(ScheduleGeneratorDTO.class);
|
||||
final HolidayDetailDTO holidayDetailDTO = Mockito.mock(HolidayDetailDTO.class);
|
||||
|
||||
final MonetaryCurrency loanCurrency = new MonetaryCurrency("USD", 2, 1);
|
||||
|
||||
final Money downPaymentMoney = Mockito.mock(Money.class);
|
||||
final Money overPaymentPortionMoney = Mockito.mock(Money.class);
|
||||
final Money calculatedMoney = Money.of(loanCurrency, BigDecimal.valueOf(500));
|
||||
|
||||
when(downPaymentMoney.getCurrencyCode()).thenReturn(loanCurrency.getCode());
|
||||
when(overPaymentPortionMoney.getCurrencyCode()).thenReturn(loanCurrency.getCode());
|
||||
|
||||
when(loanForProcessing.getLoanRepaymentScheduleDetail()).thenReturn(loanRepaymentRelatedDetail);
|
||||
when(loanRepaymentRelatedDetail.getDisbursedAmountPercentageForDownPayment()).thenReturn(BigDecimal.valueOf(10));
|
||||
when(loanForProcessing.getCurrency()).thenReturn(loanCurrency);
|
||||
when(loanCurrency.getCode()).thenReturn("CODE");
|
||||
when(loanCurrency.getCurrencyInMultiplesOf()).thenReturn(1);
|
||||
when(loanCurrency.getDigitsAfterDecimal()).thenReturn(1);
|
||||
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
|
||||
when(loanForProcessing.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
|
||||
when(loanProductRelatedDetail.getLoanScheduleType()).thenReturn(LoanScheduleType.PROGRESSIVE);
|
||||
|
||||
when(disbursement.getOverPaymentPortion(loanCurrency)).thenReturn(overPaymentPortionMoney);
|
||||
|
||||
when(downPaymentMoney.minus(overPaymentPortionMoney)).thenReturn(calculatedMoney);
|
||||
mathUtilMock.when(() -> MathUtil.negativeToZero(any(Money.class))).thenReturn(mockAdjustedDownPaymentMoney);
|
||||
when(mockAdjustedDownPaymentMoney.isGreaterThanZero()).thenReturn(false);
|
||||
|
||||
dateUtilsMock.when(DateUtils::getBusinessLocalDate).thenReturn(LocalDate.of(2024, 11, 19));
|
||||
|
||||
when(scheduleGeneratorDTO.getHolidayDetailDTO()).thenReturn(holidayDetailDTO);
|
||||
|
||||
doNothing().when(businessEventNotifierService).notifyPreBusinessEvent(any(LoanTransactionDownPaymentPreBusinessEvent.class));
|
||||
|
||||
// when
|
||||
LoanTransaction actual = underTest.handleDownPayment(scheduleGeneratorDTO, command, disbursement, loanForProcessing);
|
||||
|
||||
@@ -135,7 +233,6 @@ public class LoanDownPaymentHandlerServiceImplTest {
|
||||
verify(businessEventNotifierService, Mockito.never())
|
||||
.notifyPostBusinessEvent(Mockito.any(LoanTransactionDownPaymentPostBusinessEvent.class));
|
||||
verify(businessEventNotifierService, Mockito.never()).notifyPostBusinessEvent(Mockito.any(LoanBalanceChangedBusinessEvent.class));
|
||||
verify(loanForProcessing, Mockito.times(1)).handleDownPayment(eq(disbursement), eq(command), eq(scheduleGeneratorDTO));
|
||||
verify(loanTransactionRepository, Mockito.never()).saveAndFlush(any(LoanTransaction.class));
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -76,6 +76,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorDomainService;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||
@@ -193,6 +194,8 @@ class LoanWritePlatformServiceJpaRepositoryImplTest {
|
||||
private LoanTransactionAssembler loanTransactionAssembler;
|
||||
@Mock
|
||||
private LoanAccrualsProcessingService loanAccrualsProcessingService;
|
||||
@Mock
|
||||
private LoanChargeValidator loanChargeValidator;
|
||||
|
||||
@Test
|
||||
void givenMerchantIssuedRefundTransactionWithRelatedTransactions_whenAdjustExistingTransaction_thenRelatedTransactionsAreReversedAndEventsTriggered() {
|
||||
@@ -235,7 +238,7 @@ class LoanWritePlatformServiceJpaRepositoryImplTest {
|
||||
// Mock methods called inside adjustExistingTransaction
|
||||
when(loan.findExistingTransactionIds()).thenReturn(Collections.emptyList());
|
||||
when(loan.findExistingReversedTransactionIds()).thenReturn(Collections.emptyList());
|
||||
doNothing().when(loan).validateActivityNotBeforeClientOrGroupTransferDate(any(), any());
|
||||
doNothing().when(loanTransactionValidator).validateActivityNotBeforeClientOrGroupTransferDate(any(), any(), any());
|
||||
when(loan.isClosedWrittenOff()).thenReturn(false);
|
||||
when(loan.isClosedObligationsMet()).thenReturn(false);
|
||||
when(loan.isClosedWithOutstandingAmountMarkedForReschedule()).thenReturn(false);
|
||||
@@ -282,7 +285,7 @@ class LoanWritePlatformServiceJpaRepositoryImplTest {
|
||||
// Mock methods called inside adjustExistingTransaction
|
||||
when(loan.findExistingTransactionIds()).thenReturn(Collections.emptyList());
|
||||
when(loan.findExistingReversedTransactionIds()).thenReturn(Collections.emptyList());
|
||||
doNothing().when(loan).validateActivityNotBeforeClientOrGroupTransferDate(any(), any());
|
||||
doNothing().when(loanTransactionValidator).validateActivityNotBeforeClientOrGroupTransferDate(any(), any(), any());
|
||||
when(loan.isClosedWrittenOff()).thenReturn(false);
|
||||
when(loan.isClosedObligationsMet()).thenReturn(false);
|
||||
when(loan.isClosedWithOutstandingAmountMarkedForReschedule()).thenReturn(false);
|
||||
|
||||
Reference in New Issue
Block a user