FINERACT-2232: add income capitalization principal handling and repayment schedule calculations on loans

This commit is contained in:
mark.vituska
2025-05-27 18:35:39 +02:00
committed by Adam Saghy
parent c89ec910d8
commit aac62a92bb
38 changed files with 686 additions and 148 deletions
@@ -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()) //
@@ -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)
@@ -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")
@@ -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())
@@ -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
@@ -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> {
@@ -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) {
@@ -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>
@@ -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>