diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanOverAppliedCalculationType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanOverAppliedCalculationType.java new file mode 100644 index 000000000..9508b15f0 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanOverAppliedCalculationType.java @@ -0,0 +1,52 @@ +/** + * 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.domain; + +import java.util.Arrays; +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; + +@Getter +@RequiredArgsConstructor +public enum LoanOverAppliedCalculationType { + + FLAT("flat"), // + PERCENTAGE("percentage"), // + ; + + private final String humanReadableName; + + public static List getValuesAsEnumOptionDataList() { + return Arrays.stream(values()).map(v -> new EnumOptionData((long) (v.ordinal() + 1), v.name(), v.getHumanReadableName())).toList(); + } + + public EnumOptionData asEnumOptionData() { + return new EnumOptionData((long) this.ordinal(), this.name(), this.humanReadableName); + } + + public boolean isFlat() { + return this == FLAT; + } + + public boolean isPercentage() { + return this == PERCENTAGE; + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanMaximumAmountCalculator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanMaximumAmountCalculator.java new file mode 100644 index 000000000..70d2a9d49 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanMaximumAmountCalculator.java @@ -0,0 +1,40 @@ +/** + * 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 org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanOverAppliedCalculationType; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; +import org.springframework.stereotype.Component; + +@Component +public final class LoanMaximumAmountCalculator { + + public BigDecimal getOverAppliedMax(Loan loan) { + final LoanProduct loanProduct = loan.getLoanProduct(); + if (LoanOverAppliedCalculationType.valueOf(loanProduct.getOverAppliedCalculationType().toUpperCase()).isPercentage()) { + BigDecimal overAppliedNumber = BigDecimal.valueOf(loanProduct.getOverAppliedNumber()); + BigDecimal totalPercentage = BigDecimal.valueOf(1).add(overAppliedNumber.divide(BigDecimal.valueOf(100))); + return loan.getProposedPrincipal().multiply(totalPercentage); + } else { + return loan.getProposedPrincipal().add(BigDecimal.valueOf(loanProduct.getOverAppliedNumber())); + } + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java index 02e4739d3..51688ae08 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java @@ -43,7 +43,6 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.holiday.domain.Holiday; -import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.organisation.workingdays.domain.WorkingDays; import org.apache.fineract.portfolio.common.service.Validator; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; @@ -61,7 +60,6 @@ import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionProces import org.apache.fineract.portfolio.loanaccount.repository.LoanBuyDownFeeBalanceRepository; import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository; import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator; -import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; @Slf4j @RequiredArgsConstructor @@ -73,6 +71,7 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository; private final LoanBuyDownFeeBalanceRepository loanBuydownFeeBalanceRepository; private final LoanTransactionRepository loanTransactionRepository; + private final LoanMaximumAmountCalculator loanMaximumAmountCalculator; @Override public void validateCapitalizedIncome(final JsonCommand command, final Long loanId) { @@ -137,7 +136,7 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT final BigDecimal newTotal = totalDisbursed.add(capitalizedIncome).add(transactionAmount); if (loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { - final BigDecimal maxAppliedAmount = getOverAppliedMax(loan); + final BigDecimal maxAppliedAmount = loanMaximumAmountCalculator.getOverAppliedMax(loan); if (newTotal.compareTo(maxAppliedAmount) > 0) { baseDataValidator.reset().parameter("transactionAmount").failWithCode("exceeds.approved.amount", "Sum of disbursed amount and capitalized income can't be greater than maximum applied loan amount calculation."); @@ -595,16 +594,4 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT return new HashSet<>( Arrays.asList("transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId")); } - - private BigDecimal getOverAppliedMax(final Loan loan) { - final LoanProduct loanProduct = loan.getLoanProduct(); - if ("percentage".equals(loanProduct.getOverAppliedCalculationType())) { - final BigDecimal overAppliedNumber = BigDecimal.valueOf(loanProduct.getOverAppliedNumber()); - final BigDecimal totalPercentage = BigDecimal.valueOf(1) - .add(overAppliedNumber.divide(BigDecimal.valueOf(100L), MoneyHelper.getMathContext())); - return loan.getProposedPrincipal().multiply(totalPercentage); - } else { - return loan.getProposedPrincipal().add(BigDecimal.valueOf(loanProduct.getOverAppliedNumber())); - } - } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java index ff73f069d..291b02b8f 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java @@ -36,6 +36,7 @@ import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomeWriteP import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster; +import org.apache.fineract.portfolio.loanaccount.service.LoanMaximumAmountCalculator; import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidator; import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidatorImpl; import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService; @@ -68,9 +69,11 @@ public class ProgressiveLoanAccountConfiguration { public ProgressiveLoanTransactionValidator progressiveLoanTransactionValidator(FromJsonHelper fromApiJsonHelper, LoanTransactionValidator loanTransactionValidator, LoanRepositoryWrapper loanRepositoryWrapper, LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository, - LoanBuyDownFeeBalanceRepository loanBuydownFeeBalanceRepository, LoanTransactionRepository loanTransactionRepository) { + LoanBuyDownFeeBalanceRepository loanBuydownFeeBalanceRepository, LoanTransactionRepository loanTransactionRepository, + LoanMaximumAmountCalculator loanMaximumAmountCalculator) { return new ProgressiveLoanTransactionValidatorImpl(fromApiJsonHelper, loanTransactionValidator, loanRepositoryWrapper, - loanCapitalizedIncomeBalanceRepository, loanBuydownFeeBalanceRepository, loanTransactionRepository); + loanCapitalizedIncomeBalanceRepository, loanBuydownFeeBalanceRepository, loanTransactionRepository, + loanMaximumAmountCalculator); } @Bean diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 7d848a64b..93a73ad42 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -195,6 +195,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository; private final LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository; private final InterestRefundServiceDelegate interestRefundServiceDelegate; + private final LoanMaximumAmountCalculator loanMaximumAmountCalculator; @Override public LoanAccountData retrieveOne(final Long loanId) { @@ -492,7 +493,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa capitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loanId).stream() .map(LoanCapitalizedIncomeBalance::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); } - transactionAmount = loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount()).subtract(capitalizedIncomeBalance); + transactionAmount = loan.getLoanProduct().isAllowApprovedDisbursedAmountsOverApplied() + ? loanMaximumAmountCalculator.getOverAppliedMax(loan) + : loan.getApprovedPrincipal(); + transactionAmount = transactionAmount.subtract(loan.getDisbursedAmount()).subtract(capitalizedIncomeBalance); paymentOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes(); loanTransactionData = LoanTransactionData.loanTransactionDataForCreditTemplate( LoanEnumerations.transactionType(transactionType), DateUtils.getBusinessLocalDate(), transactionAmount, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java index 9e59127b9..9c50ac7da 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java @@ -143,6 +143,7 @@ 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.LoanJournalEntryPoster; +import org.apache.fineract.portfolio.loanaccount.service.LoanMaximumAmountCalculator; import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceImpl; @@ -347,7 +348,7 @@ public class LoanAccountConfiguration { LoanTransactionProcessingService loanTransactionProcessingService, LoanBalanceService loanBalanceService, LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository, LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository, - @Lazy InterestRefundServiceDelegate interestRefundServiceDelegate) { + @Lazy InterestRefundServiceDelegate interestRefundServiceDelegate, LoanMaximumAmountCalculator loanMaximumAmountCalculator) { return new LoanReadPlatformServiceImpl(jdbcTemplate, context, loanRepositoryWrapper, applicationCurrencyRepository, loanProductReadPlatformService, clientReadPlatformService, groupReadPlatformService, loanDropdownReadPlatformService, fundReadPlatformService, chargeReadPlatformService, codeValueReadPlatformService, calendarReadPlatformService, @@ -355,7 +356,8 @@ public class LoanAccountConfiguration { loanUtilService, configurationDomainService, accountDetailsReadPlatformService, columnValidator, sqlGenerator, delinquencyReadPlatformService, loanTransactionRepository, loanChargePaidByReadService, loanTransactionRelationReadService, loanForeclosureValidator, loanTransactionMapper, loanTransactionProcessingService, loanBalanceService, - loanCapitalizedIncomeBalanceRepository, loanBuyDownFeeBalanceRepository, interestRefundServiceDelegate); + loanCapitalizedIncomeBalanceRepository, loanBuyDownFeeBalanceRepository, interestRefundServiceDelegate, + loanMaximumAmountCalculator); } @Bean diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionTest.java index 8bc8b9c22..8bca2e261 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionTest.java @@ -101,7 +101,7 @@ public class LoanTransactionTest extends BaseLoanIntegrationTest { } @Test - public void testGetLoanTransactionTemplateForCapitalizedIncome() { + public void testGetLoanTransactionTemplateForCapitalizedIncomeWithOverAppliedAmount() { final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); final PostLoanProductsResponse loanProductsResponse = loanProductHelper @@ -114,6 +114,37 @@ public class LoanTransactionTest extends BaseLoanIntegrationTest { final String loanExternalIdStr = UUID.randomUUID().toString(); + runAt("20 December 2024", () -> { + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "20 December 2024", + 430.0, 7.0, 6, (request) -> request.externalId(loanExternalIdStr)); + + disburseLoan(loanId, BigDecimal.valueOf(230), "20 December 2024"); + + final GetLoansLoanIdTransactionsTemplateResponse transactionTemplate = loanTransactionHelper.retrieveTransactionTemplate(loanId, + capitalizedIncomeCommand, null, null, null); + + assertNotNull(transactionTemplate); + assertEquals("loanTransactionType." + capitalizedIncomeCommand, transactionTemplate.getType().getCode()); + assertEquals(transactionTemplate.getAmount(), 415); + assertThat(transactionTemplate.getPaymentTypeOptions().size() > 0); + }); + } + + @Test + public void testGetLoanTransactionTemplateForCapitalizedIncomeWithoutOverAppliedAmount() { + final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(create4IProgressive().enableIncomeCapitalization(true) + .capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT) + .capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION) + .deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue()) + .incomeFromCapitalizationAccountId(feeIncomeAccount.getAccountID().longValue()) + .capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE).overAppliedCalculationType(null) + .overAppliedNumber(null).allowApprovedDisbursedAmountsOverApplied(false)); + + final String loanExternalIdStr = UUID.randomUUID().toString(); + runAt("20 December 2024", () -> { Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "20 December 2024", 430.0, 7.0, 6, (request) -> request.externalId(loanExternalIdStr));