FINERACT-2080: Move loan validation from entity to validator classes

This commit is contained in:
Oleksii Novikov
2024-12-05 13:58:47 +01:00
committed by Adam Saghy
parent 56ee2eaaff
commit a2c01d25e5
40 changed files with 2867 additions and 2146 deletions
File diff suppressed because it is too large Load Diff
@@ -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();
@@ -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());
@@ -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());
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
}
@@ -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);
}
}
@@ -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);
}
@@ -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;
}
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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();
}
@@ -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);
}
}
}
@@ -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
);
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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));
@@ -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);
@@ -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());
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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());
}
}
}
@@ -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));
@@ -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;
}
@@ -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);
}
@@ -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.
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
@@ -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);
}
}
@@ -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();
}
@@ -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();
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
@@ -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));
}
}
@@ -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);