FINERACT-2232: add income capitalization principal handling and repayment schedule calculations on loans
This commit is contained in:
+1
@@ -464,6 +464,7 @@ public class LoanImportHandler implements ImportHandler {
|
||||
JsonObject loanRepaymentJsonob = gsonBuilder.create().toJsonTree(loanRepayments.get(rowIndex)).getAsJsonObject();
|
||||
loanRepaymentJsonob.remove("manuallyReversed");
|
||||
loanRepaymentJsonob.remove("numberOfRepayments");
|
||||
loanRepaymentJsonob.remove("reversed");
|
||||
String payload = loanRepaymentJsonob.toString();
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||
.loanRepaymentTransaction(result.getLoanId()) //
|
||||
|
||||
+9
-2
@@ -139,6 +139,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeTyp
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryBalancesRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanTemplateTypeRequiredException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.NotSupportedLoanTemplateTypeException;
|
||||
@@ -150,6 +151,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryReadPlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermVariationsRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
|
||||
@@ -297,6 +299,7 @@ public class LoansApiResource {
|
||||
private final ClientReadPlatformService clientReadPlatformService;
|
||||
private final LoanTermVariationsRepository loanTermVariationsRepository;
|
||||
private final LoanSummaryProviderDelegate loanSummaryProviderDelegate;
|
||||
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
|
||||
|
||||
/*
|
||||
* This template API is used for loan approval, ideally this should be invoked on loan that are pending for
|
||||
@@ -498,11 +501,13 @@ public class LoansApiResource {
|
||||
loanBasicDetails.getPageItems().forEach(i -> {
|
||||
if (i.getSummary() != null) {
|
||||
Collection<DisbursementData> disbursementData = this.loanReadPlatformService.retrieveLoanDisbursementDetails(i.getId());
|
||||
List<LoanTransactionRepaymentPeriodData> capitalizedIncomeData = this.loanCapitalizedIncomeBalanceRepository
|
||||
.findRepaymentPeriodDataByLoanId(i.getId());
|
||||
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData(
|
||||
i.getTimeline().getExpectedDisbursementDate(), i.getTimeline().getActualDisbursementDate(), i.getCurrency(),
|
||||
i.getPrincipal(), i.getInArrearsTolerance(), i.getFeeChargesAtDisbursementCharged());
|
||||
final LoanScheduleData repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(i.getId(),
|
||||
repaymentScheduleRelatedData, disbursementData, i.isInterestRecalculationEnabled(),
|
||||
repaymentScheduleRelatedData, disbursementData, capitalizedIncomeData, i.isInterestRecalculationEnabled(),
|
||||
LoanScheduleType.fromEnumOptionData(i.getLoanScheduleType()));
|
||||
LoanSummaryDataProvider loanSummaryDataProvider = loanSummaryProviderDelegate
|
||||
.resolveLoanSummaryDataProvider(i.getTransactionProcessingStrategyCode());
|
||||
@@ -981,13 +986,15 @@ public class LoansApiResource {
|
||||
|
||||
if (associationParameters.contains(DataTableApiConstant.repaymentScheduleAssociateParamName)) {
|
||||
mandatoryResponseParameters.add(DataTableApiConstant.repaymentScheduleAssociateParamName);
|
||||
List<LoanTransactionRepaymentPeriodData> capitalizedIncomeData = this.loanCapitalizedIncomeBalanceRepository
|
||||
.findRepaymentPeriodDataByLoanId(resolvedLoanId);
|
||||
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData(
|
||||
loanBasicDetails.getTimeline().getExpectedDisbursementDate(),
|
||||
loanBasicDetails.getTimeline().getActualDisbursementDate(), loanBasicDetails.getCurrency(),
|
||||
loanBasicDetails.getPrincipal(), loanBasicDetails.getInArrearsTolerance(),
|
||||
loanBasicDetails.getFeeChargesAtDisbursementCharged());
|
||||
repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(resolvedLoanId, repaymentScheduleRelatedData,
|
||||
disbursementData, loanBasicDetails.isInterestRecalculationEnabled(),
|
||||
disbursementData, capitalizedIncomeData, loanBasicDetails.isInterestRecalculationEnabled(),
|
||||
LoanScheduleType.fromEnumOptionData(loanBasicDetails.getLoanScheduleType()));
|
||||
|
||||
if (associationParameters.contains(DataTableApiConstant.futureScheduleAssociateParamName)
|
||||
|
||||
+4
@@ -517,7 +517,11 @@ final class LoansApiResourceSwagger {
|
||||
|
||||
public GetLoansLoanIdCurrency currency;
|
||||
@Schema(example = "1000000.000000")
|
||||
public BigDecimal totalPrincipal;
|
||||
@Schema(example = "1000000.000000")
|
||||
public BigDecimal principalDisbursed;
|
||||
@Schema(example = "1000000.000000")
|
||||
public BigDecimal totalCapitalizedIncome;
|
||||
@Schema(example = "0.000000")
|
||||
public BigDecimal principalPaid;
|
||||
@Schema(example = "0.00")
|
||||
|
||||
+3
-1
@@ -97,7 +97,9 @@ public abstract class CommonLoanSummaryDataProvider implements LoanSummaryDataPr
|
||||
}
|
||||
|
||||
return LoanSummaryData.builder().currency(defaultSummaryData.getCurrency())
|
||||
.principalDisbursed(defaultSummaryData.getPrincipalDisbursed())
|
||||
.principalDisbursed(defaultSummaryData.getPrincipalDisbursed()).totalPrincipal(defaultSummaryData.getTotalPrincipal())
|
||||
.totalCapitalizedIncome(defaultSummaryData.getTotalCapitalizedIncome())
|
||||
.totalCapitalizedIncomeAdjustment(defaultSummaryData.getTotalCapitalizedIncomeAdjustment())
|
||||
.principalAdjustments(defaultSummaryData.getPrincipalAdjustments()).principalPaid(defaultSummaryData.getPrincipalPaid())
|
||||
.principalWrittenOff(defaultSummaryData.getPrincipalWrittenOff())
|
||||
.principalOutstanding(defaultSummaryData.getPrincipalOutstanding())
|
||||
|
||||
+19
-9
@@ -21,10 +21,12 @@ package org.apache.fineract.portfolio.loanaccount.service;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
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.LoanAdjustTransactionBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
@@ -51,9 +53,10 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
||||
@Transactional
|
||||
public void processCapitalizedIncomeAmortizationOnLoanClosure(@NotNull final Loan loan) {
|
||||
final LocalDate transactionDate = getFinalCapitalizedIncomeAmortizationTransactionDate(loan);
|
||||
final LoanTransaction amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, false);
|
||||
businessEventNotifierService
|
||||
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction));
|
||||
final Optional<LoanTransaction> amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate,
|
||||
false);
|
||||
amortizationTransaction.ifPresent(loanTransaction -> businessEventNotifierService
|
||||
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(loanTransaction)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,13 +70,16 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
||||
transactionDate = DateUtils.getBusinessLocalDate();
|
||||
}
|
||||
|
||||
final LoanTransaction amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, true);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
businessEventNotifierService
|
||||
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction));
|
||||
final Optional<LoanTransaction> amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate,
|
||||
true);
|
||||
if (amortizationTransaction.isPresent()) {
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(
|
||||
new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction.get()));
|
||||
}
|
||||
}
|
||||
|
||||
private LoanTransaction createCapitalizedIncomeAmortizationTransaction(final Loan loan, final LocalDate transactionDate,
|
||||
private Optional<LoanTransaction> createCapitalizedIncomeAmortizationTransaction(final Loan loan, final LocalDate transactionDate,
|
||||
final boolean isChargeOff) {
|
||||
ExternalId externalId = ExternalId.empty();
|
||||
BigDecimal totalUnrecognizedAmount = BigDecimal.ZERO;
|
||||
@@ -92,13 +98,17 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
||||
balance.setUnrecognizedAmount(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
if (MathUtil.isLessThanOrEqualZero(totalUnrecognizedAmount)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final LoanTransaction amortizationTransaction = LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(),
|
||||
transactionDate, totalUnrecognizedAmount, externalId);
|
||||
|
||||
loan.addLoanTransaction(amortizationTransaction);
|
||||
loanTransactionRepository.saveAndFlush(amortizationTransaction);
|
||||
|
||||
return amortizationTransaction;
|
||||
return Optional.of(amortizationTransaction);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+113
-27
@@ -98,6 +98,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanSchedulePeriodDataWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
|
||||
@@ -118,6 +119,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
|
||||
@@ -249,8 +251,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
accountData.getFeeChargesAtDisbursementCharged());
|
||||
|
||||
final Collection<DisbursementData> disbursementData = retrieveLoanDisbursementDetails(accountData.getId());
|
||||
List<LoanTransactionRepaymentPeriodData> capitalizedIncomeData = loanCapitalizedIncomeBalanceRepository
|
||||
.findRepaymentPeriodDataByLoanId(accountData.getId());
|
||||
final LoanScheduleData repaymentSchedule = retrieveRepaymentSchedule(accountData.getId(), repaymentScheduleRelatedData,
|
||||
disbursementData, accountData.isInterestRecalculationEnabled(),
|
||||
disbursementData, capitalizedIncomeData, accountData.isInterestRecalculationEnabled(),
|
||||
LoanScheduleType.fromEnumOptionData(accountData.getLoanScheduleType()));
|
||||
accountData.setRepaymentSchedule(repaymentSchedule);
|
||||
return accountData;
|
||||
@@ -259,13 +263,15 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
@Override
|
||||
public LoanScheduleData retrieveRepaymentSchedule(final Long loanId,
|
||||
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData, Collection<DisbursementData> disbursementData,
|
||||
boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType) {
|
||||
Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData, boolean isInterestRecalculationEnabled,
|
||||
LoanScheduleType loanScheduleType) {
|
||||
|
||||
try {
|
||||
this.context.authenticatedUser();
|
||||
|
||||
final LoanScheduleResultSetExtractor fullResultsetExtractor = new LoanScheduleResultSetExtractor(
|
||||
repaymentScheduleRelatedLoanData, disbursementData, isInterestRecalculationEnabled, loanScheduleType);
|
||||
repaymentScheduleRelatedLoanData, disbursementData, capitalizedIncomeData, isInterestRecalculationEnabled,
|
||||
loanScheduleType);
|
||||
final String sql = "select " + fullResultsetExtractor.schema() + " where ls.loan_id = ? order by ls.loan_id, ls.installment";
|
||||
|
||||
return this.jdbcTemplate.query(sql, fullResultsetExtractor, loanId); // NOSONAR
|
||||
@@ -749,7 +755,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
+ " l.expected_disbursedon_date as expectedDisbursementDate, l.disbursedon_date as actualDisbursementDate, dbu.username as disbursedByUsername, dbu.firstname as disbursedByFirstname, dbu.lastname as disbursedByLastname,"
|
||||
+ " l.closedon_date as closedOnDate, cbu.username as closedByUsername, cbu.firstname as closedByFirstname, cbu.lastname as closedByLastname, l.writtenoffon_date as writtenOffOnDate, "
|
||||
+ " l.expected_firstrepaymenton_date as expectedFirstRepaymentOnDate, l.interest_calculated_from_date as interestChargedFromDate, l.maturedon_date as actualMaturityDate, l.expected_maturedon_date as expectedMaturityDate, "
|
||||
+ " l.principal_amount_proposed as proposedPrincipal, l.principal_amount as principal, l.approved_principal as approvedPrincipal, l.net_disbursal_amount as netDisbursalAmount, l.arrearstolerance_amount as inArrearsTolerance, l.number_of_repayments as numberOfRepayments, l.repay_every as repaymentEvery,"
|
||||
+ " l.principal_amount_proposed as proposedPrincipal, l.principal_amount as principal, l.total_principal_derived as totalPrincipal, l.approved_principal as approvedPrincipal, l.net_disbursal_amount as netDisbursalAmount, l.arrearstolerance_amount as inArrearsTolerance, l.number_of_repayments as numberOfRepayments, l.repay_every as repaymentEvery,"
|
||||
+ " l.grace_on_principal_periods as graceOnPrincipalPayment, l.recurring_moratorium_principal_periods as recurringMoratoriumOnPrincipalPeriods, l.grace_on_interest_periods as graceOnInterestPayment, l.grace_interest_free_periods as graceOnInterestCharged,l.grace_on_arrears_ageing as graceOnArrearsAgeing,"
|
||||
+ " l.nominal_interest_rate_per_period as interestRatePerPeriod, l.annual_nominal_interest_rate as annualInterestRate, "
|
||||
+ " l.repayment_period_frequency_enum as repaymentFrequencyType, l.interest_period_frequency_enum as interestRateFrequencyType, "
|
||||
@@ -764,7 +770,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
+ sqlGenerator.escape("name")
|
||||
+ " as currencyName, rc.display_symbol as currencyDisplaySymbol, rc.internationalized_name_code as currencyNameCode, "
|
||||
+ " l.loan_officer_id as loanOfficerId, s.display_name as loanOfficerName, "
|
||||
+ " l.principal_disbursed_derived as principalDisbursed, l.principal_repaid_derived as principalPaid,"
|
||||
+ " l.principal_disbursed_derived as principalDisbursed, l.capitalized_income_derived as totalCapitalizedIncome, l.capitalized_income_adjustment_derived as totalCapitalizedIncomeAdjustment, l.principal_repaid_derived as principalPaid,"
|
||||
+ " l.principal_adjustments_derived as principalAdjustments, l.principal_writtenoff_derived as principalWrittenOff,"
|
||||
+ " l.fee_adjustments_derived as feeAdjustments, l.penalty_adjustments_derived as penaltyAdjustments,"
|
||||
+ " l.principal_outstanding_derived as principalOutstanding, l.interest_charged_derived as interestCharged,"
|
||||
@@ -1023,6 +1029,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
|
||||
// loan summary
|
||||
final BigDecimal principalDisbursed = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalDisbursed");
|
||||
final BigDecimal totalPrincipal = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalPrincipal");
|
||||
final BigDecimal totalCapitalizedIncome = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalCapitalizedIncome");
|
||||
final BigDecimal totalCapitalizedIncomeAdjustment = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs,
|
||||
"totalCapitalizedIncomeAdjustment");
|
||||
final BigDecimal principalAdjustments = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalAdjustments");
|
||||
final BigDecimal principalPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalPaid");
|
||||
final BigDecimal principalWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalWrittenOff");
|
||||
@@ -1066,12 +1076,14 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
inArrears = (overdueSinceDate != null);
|
||||
|
||||
loanSummary = LoanSummaryData.builder().currency(currencyData).principalDisbursed(principalDisbursed)
|
||||
.principalAdjustments(principalAdjustments).principalPaid(principalPaid).principalWrittenOff(principalWrittenOff)
|
||||
.principalOutstanding(principalOutstanding).principalOverdue(principalOverdue).interestCharged(interestCharged)
|
||||
.interestPaid(interestPaid).interestWaived(interestWaived).interestWrittenOff(interestWrittenOff)
|
||||
.interestOutstanding(interestOutstanding).interestOverdue(interestOverdue).feeChargesCharged(feeChargesCharged)
|
||||
.feeAdjustments(feeAdjustments).feeChargesDueAtDisbursementCharged(feeChargesDueAtDisbursementCharged)
|
||||
.feeChargesPaid(feeChargesPaid).feeChargesWaived(feeChargesWaived).feeChargesWrittenOff(feeChargesWrittenOff)
|
||||
.totalPrincipal(totalPrincipal).totalCapitalizedIncome(totalCapitalizedIncome)
|
||||
.totalCapitalizedIncomeAdjustment(totalCapitalizedIncomeAdjustment).principalAdjustments(principalAdjustments)
|
||||
.principalPaid(principalPaid).principalWrittenOff(principalWrittenOff).principalOutstanding(principalOutstanding)
|
||||
.principalOverdue(principalOverdue).interestCharged(interestCharged).interestPaid(interestPaid)
|
||||
.interestWaived(interestWaived).interestWrittenOff(interestWrittenOff).interestOutstanding(interestOutstanding)
|
||||
.interestOverdue(interestOverdue).feeChargesCharged(feeChargesCharged).feeAdjustments(feeAdjustments)
|
||||
.feeChargesDueAtDisbursementCharged(feeChargesDueAtDisbursementCharged).feeChargesPaid(feeChargesPaid)
|
||||
.feeChargesWaived(feeChargesWaived).feeChargesWrittenOff(feeChargesWrittenOff)
|
||||
.feeChargesOutstanding(feeChargesOutstanding).feeChargesOverdue(feeChargesOverdue)
|
||||
.penaltyChargesCharged(penaltyChargesCharged).penaltyAdjustments(penaltyAdjustments)
|
||||
.penaltyChargesPaid(penaltyChargesPaid).penaltyChargesWaived(penaltyChargesWaived)
|
||||
@@ -1271,15 +1283,18 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
private final DisbursementData disbursement;
|
||||
private final BigDecimal totalFeeChargesDueAtDisbursement;
|
||||
private final Collection<DisbursementData> disbursementData;
|
||||
private final Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData;
|
||||
private final LoanScheduleType loanScheduleType;
|
||||
private LocalDate lastDueDate;
|
||||
private BigDecimal outstandingLoanPrincipalBalance;
|
||||
private boolean excludePastUnDisbursed;
|
||||
|
||||
LoanScheduleResultSetExtractor(final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData,
|
||||
Collection<DisbursementData> disbursementData, boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType) {
|
||||
Collection<DisbursementData> disbursementData, Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData,
|
||||
boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType) {
|
||||
this.currency = repaymentScheduleRelatedLoanData.getCurrency();
|
||||
this.disbursement = repaymentScheduleRelatedLoanData.disbursementData();
|
||||
this.capitalizedIncomeData = capitalizedIncomeData;
|
||||
this.totalFeeChargesDueAtDisbursement = repaymentScheduleRelatedLoanData.getTotalFeeChargesAtDisbursement();
|
||||
this.lastDueDate = this.disbursement.disbursementDate();
|
||||
this.outstandingLoanPrincipalBalance = this.disbursement.getPrincipal();
|
||||
@@ -1359,10 +1374,15 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate");
|
||||
final LocalDate obligationsMetOnDate = JdbcSupport.getLocalDate(rs, "obligationsMetOnDate");
|
||||
final boolean complete = rs.getBoolean("complete");
|
||||
BigDecimal disbursedAmount = BigDecimal.ZERO;
|
||||
|
||||
disbursedAmount = processDisbursementData(loanScheduleType, disbursementData, fromDate, dueDate, disbursementPeriodIds,
|
||||
disbursementChargeAmount, waivedChargeAmount, periods);
|
||||
List<LoanSchedulePeriodDataWrapper> combinedDataList = new ArrayList<>();
|
||||
combinedDataList.addAll(
|
||||
collectEligibleDisbursementData(loanScheduleType, disbursementData, fromDate, dueDate, disbursementPeriodIds));
|
||||
combinedDataList.addAll(collectEligibleCapitalizedIncomeData(fromDate, dueDate, disbursementPeriodIds));
|
||||
combinedDataList.sort(this::sortPeriodDataHolders);
|
||||
fillLoanSchedulePeriodData(periods, combinedDataList, disbursementChargeAmount, waivedChargeAmount);
|
||||
|
||||
BigDecimal disbursedAmount = calculateDisbursedAmount(combinedDataList);
|
||||
|
||||
// Add the Charge back or Credits to the initial amount to avoid negative balance
|
||||
final BigDecimal principalCredits = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalCredits");
|
||||
@@ -1473,26 +1493,82 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
totalPaidLate.getAmount(), totalOutstanding.getAmount(), totalCredits.getAmount());
|
||||
}
|
||||
|
||||
private BigDecimal processDisbursementData(LoanScheduleType loanScheduleType, Collection<DisbursementData> disbursementData,
|
||||
LocalDate fromDate, LocalDate dueDate, Set<Long> disbursementPeriodIds, BigDecimal disbursementChargeAmount,
|
||||
BigDecimal waivedChargeAmount, List<LoanSchedulePeriodData> periods) {
|
||||
BigDecimal disbursedAmount = BigDecimal.ZERO;
|
||||
private List<LoanSchedulePeriodDataWrapper> collectEligibleDisbursementData(LoanScheduleType loanScheduleType,
|
||||
Collection<DisbursementData> disbursementData, LocalDate fromDate, LocalDate dueDate, Set<Long> disbursementPeriodIds) {
|
||||
List<LoanSchedulePeriodDataWrapper> disbursementDataList = new ArrayList<>();
|
||||
// Collect eligible disbursement data
|
||||
for (final DisbursementData data : disbursementData) {
|
||||
boolean isDueForDisbursement = data.isDueForDisbursement(loanScheduleType, fromDate, dueDate);
|
||||
if (((fromDate.equals(this.disbursement.disbursementDate()) && data.disbursementDate().equals(fromDate))
|
||||
boolean isEligible = ((fromDate.equals(this.disbursement.disbursementDate()) && data.disbursementDate().equals(fromDate))
|
||||
|| (fromDate.equals(dueDate) && data.disbursementDate().equals(fromDate))
|
||||
|| canAddDisbursementData(data, isDueForDisbursement, excludePastUnDisbursed))
|
||||
&& !disbursementPeriodIds.contains(data.getId())) {
|
||||
disbursedAmount = disbursedAmount.add(data.getPrincipal());
|
||||
LoanSchedulePeriodData periodData = createLoanSchedulePeriodData(data, disbursementChargeAmount, waivedChargeAmount);
|
||||
periods.add(periodData);
|
||||
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(periodData.getPrincipalDisbursed());
|
||||
&& !disbursementPeriodIds.contains(data.getId());
|
||||
|
||||
if (isEligible) {
|
||||
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
|
||||
disbursementPeriodIds.add(data.getId());
|
||||
}
|
||||
}
|
||||
return disbursementDataList;
|
||||
}
|
||||
|
||||
private List<LoanSchedulePeriodDataWrapper> collectEligibleCapitalizedIncomeData(LocalDate fromDate, LocalDate dueDate,
|
||||
Set<Long> disbursementPeriodIds) {
|
||||
List<LoanSchedulePeriodDataWrapper> capitalizedIncomeDataList = new ArrayList<>();
|
||||
// Collect eligible capitalized income data
|
||||
for (LoanTransactionRepaymentPeriodData data : capitalizedIncomeData) {
|
||||
boolean isEligible = canAddCapitalizedIncomeData(data, fromDate, dueDate)
|
||||
&& !disbursementPeriodIds.contains(data.getTransactionId());
|
||||
|
||||
if (isEligible) {
|
||||
capitalizedIncomeDataList.add(new LoanSchedulePeriodDataWrapper(data, data.getDate(), false));
|
||||
disbursementPeriodIds.add(data.getTransactionId());
|
||||
}
|
||||
}
|
||||
return capitalizedIncomeDataList;
|
||||
}
|
||||
|
||||
private void fillLoanSchedulePeriodData(List<LoanSchedulePeriodData> periods, List<LoanSchedulePeriodDataWrapper> combinedDataList,
|
||||
BigDecimal disbursementChargeAmount, BigDecimal waivedChargeAmount) {
|
||||
// Process all collected data in chronological order
|
||||
for (LoanSchedulePeriodDataWrapper dataItem : combinedDataList) {
|
||||
LoanSchedulePeriodData periodData;
|
||||
if (dataItem.isDisbursement()) {
|
||||
// Process disbursement data
|
||||
DisbursementData data = (DisbursementData) dataItem.getData();
|
||||
periodData = createLoanSchedulePeriodData(data, disbursementChargeAmount, waivedChargeAmount);
|
||||
} else {
|
||||
// Process capitalized income data
|
||||
LoanTransactionRepaymentPeriodData data = (LoanTransactionRepaymentPeriodData) dataItem.getData();
|
||||
periodData = createLoanSchedulePeriodData(data);
|
||||
}
|
||||
|
||||
// Common processing for both data types
|
||||
periods.add(periodData);
|
||||
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(periodData.getPrincipalDisbursed());
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal calculateDisbursedAmount(List<LoanSchedulePeriodDataWrapper> combinedDataList) {
|
||||
BigDecimal disbursedAmount = BigDecimal.ZERO;
|
||||
for (LoanSchedulePeriodDataWrapper dataItem : combinedDataList) {
|
||||
if (dataItem.isDisbursement()) {
|
||||
DisbursementData data = (DisbursementData) dataItem.getData();
|
||||
disbursedAmount = disbursedAmount.add(data.getPrincipal());
|
||||
}
|
||||
}
|
||||
return disbursedAmount;
|
||||
}
|
||||
|
||||
private int sortPeriodDataHolders(LoanSchedulePeriodDataWrapper item1, LoanSchedulePeriodDataWrapper item2) {
|
||||
int dateComparison = item1.getDate().compareTo(item2.getDate());
|
||||
if (dateComparison == 0 && item1.isDisbursement() != item2.isDisbursement()) {
|
||||
// If dates are equal, prioritize disbursement data
|
||||
return item1.isDisbursement() ? -1 : 1;
|
||||
}
|
||||
return dateComparison;
|
||||
}
|
||||
|
||||
private LoanSchedulePeriodData createLoanSchedulePeriodData(final DisbursementData data, BigDecimal disbursementChargeAmount,
|
||||
BigDecimal waivedChargeAmount) {
|
||||
BigDecimal chargeAmount = data.getChargeAmount() == null ? disbursementChargeAmount
|
||||
@@ -1501,11 +1577,20 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
data.isDisbursed());
|
||||
}
|
||||
|
||||
private LoanSchedulePeriodData createLoanSchedulePeriodData(final LoanTransactionRepaymentPeriodData data) {
|
||||
BigDecimal feeCharges = Objects.isNull(data.getFeeChargesPortion()) ? BigDecimal.ZERO : data.getFeeChargesPortion();
|
||||
return LoanSchedulePeriodData.disbursementOnlyPeriod(data.getDate(), data.getAmount(), feeCharges, !data.isReversed());
|
||||
}
|
||||
|
||||
private boolean canAddDisbursementData(DisbursementData data, boolean isDueForDisbursement, boolean excludePastUnDisbursed) {
|
||||
return (!excludePastUnDisbursed || data.isDisbursed() || !DateUtils.isBeforeBusinessDate(data.disbursementDate()))
|
||||
&& isDueForDisbursement;
|
||||
}
|
||||
|
||||
private boolean canAddCapitalizedIncomeData(LoanTransactionRepaymentPeriodData data, LocalDate fromDate, LocalDate dueDate) {
|
||||
return !data.isReversed() && DateUtils.isDateInRangeFromInclusiveToExclusive(fromDate, dueDate, data.getDate());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class LoanTransactionsMapper implements RowMapper<LoanTransactionData> {
|
||||
@@ -2168,7 +2253,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REPAYMENT);
|
||||
final Collection<PaymentTypeData> paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
|
||||
final BigDecimal outstandingLoanBalance = loanRepaymentScheduleInstallment.getPrincipalOutstanding(currency).getAmount();
|
||||
final Boolean isReversed = false;
|
||||
final Boolean isManuallyReversed = false;
|
||||
|
||||
final Money outStandingAmount = loanRepaymentScheduleInstallment.getTotalOutstanding(currency);
|
||||
|
||||
@@ -2178,7 +2263,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
loanRepaymentScheduleInstallment.getInterestOutstanding(currency).getAmount(),
|
||||
loanRepaymentScheduleInstallment.getFeeChargesOutstanding(currency).getAmount(),
|
||||
loanRepaymentScheduleInstallment.getPenaltyChargesOutstanding(currency).getAmount(), null, unrecognizedIncomePortion,
|
||||
paymentTypeOptions, ExternalId.empty(), null, null, outstandingLoanBalance, isReversed, loanId, loan.getExternalId());
|
||||
paymentTypeOptions, ExternalId.empty(), null, null, outstandingLoanBalance, isManuallyReversed, loanId,
|
||||
loan.getExternalId());
|
||||
}
|
||||
|
||||
private static final class CurrencyMapper implements RowMapper<CurrencyData> {
|
||||
|
||||
+9
-2
@@ -64,6 +64,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
|
||||
import org.apache.fineract.portfolio.note.domain.Note;
|
||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
@@ -93,6 +94,7 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
|
||||
private final LoanChargeValidator loanChargeValidator;
|
||||
private final LoanJournalEntryPoster journalEntryPoster;
|
||||
private final LoanBalanceService loanBalanceService;
|
||||
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult adjustLoanTransaction(Loan loan, LoanTransaction transactionToAdjust, LoanAdjustmentParameter parameter,
|
||||
@@ -245,8 +247,9 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
|
||||
transactionForAdjustment.getTransactionDate());
|
||||
|
||||
if (!transactionForAdjustment.isAccrualRelated() && transactionForAdjustment.isNotRepaymentLikeType()
|
||||
&& transactionForAdjustment.isNotWaiver() && transactionForAdjustment.isNotCreditBalanceRefund()) {
|
||||
final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver, accrual or credit balance refund can be adjusted.";
|
||||
&& transactionForAdjustment.isNotWaiver() && transactionForAdjustment.isNotCreditBalanceRefund()
|
||||
&& !transactionForAdjustment.isCapitalizedIncome()) {
|
||||
final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver, capitalized income, accrual or credit balance refund can be adjusted.";
|
||||
throw new InvalidLoanTransactionTypeException("transaction",
|
||||
"adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.transactions", errorMessage);
|
||||
}
|
||||
@@ -285,6 +288,10 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
|
||||
loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, newTransactionDetail, transactionForAdjustment,
|
||||
scheduleGeneratorDTO);
|
||||
}
|
||||
|
||||
if (transactionForAdjustment.getTypeOf().equals(LoanTransactionType.CAPITALIZED_INCOME)) {
|
||||
reprocessLoanTransactionsService.reprocessTransactions(loan);
|
||||
}
|
||||
}
|
||||
|
||||
private Loan saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) {
|
||||
|
||||
+7
-3
@@ -204,9 +204,13 @@ public class NoteWritePlatformServiceJpaRepositoryImpl implements NoteWritePlatf
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createLoanNote(final Long loanId, final String note) {
|
||||
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId);
|
||||
final Note newNote = Note.loanNote(loan, note);
|
||||
public void createLoanTransactionNote(final Long loanTransactionId, final String note) {
|
||||
final LoanTransaction loanTransaction = this.loanTransactionRepository.findById(loanTransactionId)
|
||||
.orElseThrow(() -> new LoanTransactionNotFoundException(loanTransactionId));
|
||||
|
||||
final Loan loan = loanTransaction.getLoan();
|
||||
|
||||
final Note newNote = Note.loanTransactionNote(loan, loanTransaction, note);
|
||||
|
||||
this.noteRepository.save(newNote);
|
||||
}
|
||||
|
||||
@@ -196,4 +196,5 @@
|
||||
<include file="parts/0175_add_fk_acc_product_mapping.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0176_add_capitalized_income_amortization_transaction.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0177_acc_journal_entry_index.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0178_add_principal_from_capitalized_income_to_loan_summary.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||
<changeSet author="fineract" id="1">
|
||||
<addColumn tableName="m_loan">
|
||||
<column defaultValueNumeric="0.000000" name="capitalized_income_derived" type="DECIMAL(19, 6)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column defaultValueNumeric="0.000000" name="capitalized_income_adjustment_derived" type="DECIMAL(19, 6)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column defaultValueNumeric="0.000000" name="total_principal_derived" type="DECIMAL(19, 6)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user