diff --git a/fineract-doc/src/docs/en/introduction.adoc b/fineract-doc/src/docs/en/introduction.adoc index 6dcb703a227..6e68b178405 100644 --- a/fineract-doc/src/docs/en/introduction.adoc +++ b/fineract-doc/src/docs/en/introduction.adoc @@ -8,6 +8,9 @@ Fineract provides a reliable, robust, and affordable solution for entrepreneurs, Fineract 1.x is a mature platform with open APIs. +``` +image:https://static.scarf.sh/a.png?x-pxid=c8fb6966-9836-4ad6-882f-0461000fcbc7[] +``` diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/command/LoanChargeCommand.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/command/LoanChargeCommand.java index 0cb23100a02..08e46bc718f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/command/LoanChargeCommand.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/command/LoanChargeCommand.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.loanaccount.command; +import java.io.Serial; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDate; @@ -30,6 +31,9 @@ @Data public class LoanChargeCommand implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + @SuppressWarnings("unused") private Long id; private Long chargeId; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 3c8677bb2d2..8f73455c820 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -1392,9 +1392,7 @@ private void applyPeriodicAccruals(final Collection accruals) { Money fee = Money.zero(getCurrency()); Money penality = Money.zero(getCurrency()); for (LoanTransaction loanTransaction : accruals) { - LocalDate transactionDateForRange = isBasedOnSubmittedOnDate - ? loanTransaction.getLoanChargesPaid().stream().findFirst().get().getLoanCharge().getDueDate() - : loanTransaction.getTransactionDate(); + LocalDate transactionDateForRange = getDateForRangeCalculation(loanTransaction, isBasedOnSubmittedOnDate); boolean isInPeriod = LoanRepaymentScheduleProcessingWrapper.isInPeriod(transactionDateForRange, installment, installments); if (isInPeriod) { interest = interest.plus(loanTransaction.getInterestPortion(getCurrency())); @@ -1423,6 +1421,12 @@ private void applyPeriodicAccruals(final Collection accruals) { } } + private LocalDate getDateForRangeCalculation(LoanTransaction loanTransaction, boolean isChargeAccrualBasedOnSubmittedOnDate) { + return isChargeAccrualBasedOnSubmittedOnDate && !loanTransaction.getLoanChargesPaid().isEmpty() + ? loanTransaction.getLoanChargesPaid().stream().findFirst().get().getLoanCharge().getEffectiveDueDate() + : loanTransaction.getTransactionDate(); + } + private void updateAccrualsForNonPeriodicAccruals(final Collection accruals) { final Money interestApplied = Money.of(getCurrency(), this.summary.getTotalInterestCharged()); ExternalId externalId = ExternalId.empty(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java index 2eb2241f30d..781d3eab47d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java @@ -84,6 +84,7 @@ import org.apache.fineract.portfolio.loanproduct.serialization.LoanProductDataValidator; import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService; import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; @@ -151,6 +152,7 @@ public final class LoanApplicationValidator { private final LoanCollateralAssembler collateralAssembler; private final WorkingDaysRepositoryWrapper workingDaysRepository; private final HolidayRepository holidayRepository; + private final SavingsAccountRepositoryWrapper savingsAccountRepository; public void validateForCreate(final Loan loan) { final LocalDate expectedFirstRepaymentOnDate = loan.getExpectedFirstRepaymentOnDate(); @@ -207,7 +209,6 @@ private void validateForCreate(final JsonElement element) { final Long groupId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.groupIdParameterName, element); final Client client = clientId != null ? this.clientRepository.findOneWithNotFoundDetection(clientId) : null; final Group group = groupId != null ? this.groupRepository.findOneWithNotFoundDetection(groupId) : null; - validateClientOrGroup(client, group, productId); validateOrThrow("loan", baseDataValidator -> { @@ -257,7 +258,6 @@ private void validateForCreate(final JsonElement element) { } } - } boolean isEqualAmortization = false; @@ -483,11 +483,7 @@ private void validateForCreate(final JsonElement element) { validateTransactionProcessingStrategy(transactionProcessingStrategy, loanProduct, baseDataValidator); - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.linkAccountIdParameterName, element)) { - final Long linkAccountId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.linkAccountIdParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.linkAccountIdParameterName).value(linkAccountId).ignoreIfNull() - .longGreaterThanZero(); - } + validateLinkedSavingsAccount(element, baseDataValidator); if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.createStandingInstructionAtDisbursementParameterName, element)) { final Boolean createStandingInstructionAtDisbursement = this.fromApiJsonHelper @@ -503,7 +499,7 @@ private void validateForCreate(final JsonElement element) { // charges loanChargeApiJsonValidator.validateLoanCharges(element, loanProduct, baseDataValidator); - /* + /** * TODO: Add collaterals for other loan accounts if needed. For now it's only applicable for individual * accounts. (loanType.isJLG() || loanType.isGLIM()) */ @@ -700,7 +696,7 @@ private void validateForCreate(final JsonElement element) { } checkForProductMixRestrictions(element); - validateSubmittedOnDate(element, loanProduct); + validateSubmittedOnDate(element, null, loanProduct); validateDisbursementDetails(loanProduct, element); validateCollateral(element); // validate if disbursement date is a holiday or a non-working day @@ -1053,7 +1049,7 @@ public void validateForModify(final JsonCommand command, final Loan loan) { "Principal fixing cannot be done with equal installment amortization"); } - LocalDate expectedDisbursementDate = null; + LocalDate expectedDisbursementDate = loan.getExpectedDisbursementDate(); if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.expectedDisbursementDateParameterName, element)) { atLeastOneParameterPassedForUpdate = true; @@ -1135,12 +1131,7 @@ public void validateForModify(final JsonCommand command, final Loan loan) { .notExceedingLengthOf(500); } - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.submittedOnNoteParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final Long linkAccountId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.linkAccountIdParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.linkAccountIdParameterName).value(linkAccountId).ignoreIfNull() - .longGreaterThanZero(); - } + validateLinkedSavingsAccount(element, baseDataValidator); // charges loanChargeApiJsonValidator.validateLoanCharges(element, loanProduct, baseDataValidator); @@ -1360,7 +1351,7 @@ public void validateForModify(final JsonCommand command, final Loan loan) { } validateDisbursementDetails(loanProduct, element); - validateSubmittedOnDate(element, loanProduct); + validateSubmittedOnDate(element, loan.getSubmittedOnDate(), loanProduct); validateClientOrGroup(client, group, productId); @@ -1504,21 +1495,29 @@ private void validateLoanTermAndRepaidEveryValues(final Integer loanTermFrequenc } } - public void validatelinkedSavingsAccount(final SavingsAccount savingsAccount, final Loan loanApplication) { - final List dataValidationErrors = new ArrayList<>(); - if (savingsAccount.isNotActive()) { - dataValidationErrors.add(ApiParameterError.parameterError("validation.msg.loan.linked.savings.account.is.not.active", - "Linked Savings account with id:" + savingsAccount.getId() + " is not in active state", "linkAccountId", - savingsAccount.getId())); - } else if (!loanApplication.getClientId().equals(savingsAccount.clientId())) { - dataValidationErrors - .add(ApiParameterError.parameterError("validation.msg.loan.linked.savings.account.not.belongs.to.same.client", - "Linked Savings account with id:" + savingsAccount.getId() + " is not belongs to the same client", - "linkAccountId", savingsAccount.getId())); - } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); + public void validateLinkedSavingsAccount(final JsonElement element, DataValidatorBuilder baseDataValidator) { + final boolean linkedAccountIdWasProvided = this.fromApiJsonHelper.parameterExists(LoanApiConstants.linkAccountIdParameterName, + element); + if (linkedAccountIdWasProvided) { + final Long linkAccountId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.linkAccountIdParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.linkAccountIdParameterName).value(linkAccountId).ignoreIfNull() + .longGreaterThanZero(); + + final Long linkedAccountId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.linkAccountIdParameterName, element); + final SavingsAccount savingsAccount = savingsAccountRepository.findOneWithNotFoundDetection(linkedAccountId); + final Long clientId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.clientIdParameterName, element); + if (savingsAccount.isNotActive()) { + final ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.linked.savings.account.is.not.active", + "Linked Savings account with id:" + savingsAccount.getId() + " is not in active state", "linkAccountId", + savingsAccount.getId()); + baseDataValidator.getDataValidationErrors().add(error); + } else if (!clientId.equals(savingsAccount.clientId())) { + final ApiParameterError error = ApiParameterError.parameterError( + "validation.msg.loan.linked.savings.account.not.belongs.to.same.client", + "Linked Savings account with id:" + savingsAccount.getId() + " is not belongs to the same client", "linkAccountId", + savingsAccount.getId()); + baseDataValidator.getDataValidationErrors().add(error); + } } } @@ -1623,9 +1622,7 @@ public void validateLoanMultiDisbursementDate(final JsonElement element, final D .integerSameAsNumber(InterestMethod.DECLINING_BALANCE.getValue()); } - } - } public void validateLoanForCollaterals(final Loan loan, final BigDecimal total) { @@ -1770,11 +1767,12 @@ private void checkForProductMixRestrictions(final List activeLoansLoanProd } } - private void validateSubmittedOnDate(final JsonElement element, LoanProduct loanProduct) { + private void validateSubmittedOnDate(final JsonElement element, LocalDate originalSubmittedOnDate, LoanProduct loanProduct) { final LocalDate startDate = loanProduct.getStartDate(); final LocalDate closeDate = loanProduct.getCloseDate(); - final LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.submittedOnDateParameterName, - element); + final LocalDate submittedOnDate = this.fromApiJsonHelper.parameterExists(LoanApiConstants.submittedOnDateParameterName, element) + ? this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.submittedOnDateParameterName, element) + : originalSubmittedOnDate; final Long clientId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.clientIdParameterName, element); final Long groupId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.groupIdParameterName, element); final LocalDate expectedDisbursementDate = this.fromApiJsonHelper diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java index 8266cddcc16..f239e934ea3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -6,9 +6,9 @@ * 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 @@ -34,10 +34,13 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.exception.ErrorHandler; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -78,7 +81,7 @@ import org.apache.fineract.portfolio.note.domain.NoteRepository; import org.apache.fineract.portfolio.savings.data.GroupSavingsIndividualMonitoringAccountData; import org.apache.fineract.portfolio.savings.domain.SavingsAccount; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; import org.apache.fineract.portfolio.savings.service.GSIMReadPlatformService; import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.dao.DataIntegrityViolationException; @@ -100,7 +103,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; private final CalendarRepository calendarRepository; private final CalendarInstanceRepository calendarInstanceRepository; - private final SavingsAccountAssembler savingsAccountAssembler; + private final SavingsAccountRepositoryWrapper savingsAccountRepository; private final AccountAssociationsRepository accountAssociationsRepository; private final LoanReadPlatformService loanReadPlatformService; private final BusinessEventNotifierService businessEventNotifierService; @@ -261,8 +264,7 @@ public CommandProcessingResult modifyApplication(final Long loanId, final JsonCo final Long calendarId = command.longValueOfParameterNamed("calendarId"); modifyCalendar(loanId, calendarId, loan, changes); // Save linked account information - final Long savingsAccountId = command.longValueOfParameterNamed(LoanApiConstants.linkAccountIdParameterName); - modifyLinkedAccount(loanId, savingsAccountId, changes, loan); + modifyLinkedAccount(command, changes, loan); // updating loan interest recalculation details throwing null // pointer exception after saveAndFlush @@ -293,41 +295,50 @@ public CommandProcessingResult modifyApplication(final Long loanId, final JsonCo } } - private void modifyLinkedAccount(Long loanId, Long savingsAccountId, Map changes, Loan loan) { - final boolean backdatedTxnsAllowedTill = false; - AccountAssociations accountAssociations = this.accountAssociationsRepository.findByLoanIdAndType(loanId, - AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue()); - if (savingsAccountId == null) { - if (accountAssociations != null) { - this.accountAssociationsRepository.delete(accountAssociations); - changes.put(LoanApiConstants.linkAccountIdParameterName, null); - } - } else { - boolean isModified = false; - if (accountAssociations == null) { - isModified = true; + private void modifyLinkedAccount(JsonCommand command, Map changes, Loan loan) { + final Long savingsAccountId = command.longValueOfParameterNamed(LoanApiConstants.linkAccountIdParameterName); + final boolean linkedAccountWasProvided = command.parameterExists(LoanApiConstants.linkAccountIdParameterName); + // Only process if something was provided + if (linkedAccountWasProvided) { + AccountAssociations accountAssociations = this.accountAssociationsRepository.findByLoanIdAndType(loan.getId(), + AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue()); + // Explicit null value was provided as linked account id, so we shall remove the association + if (savingsAccountId == null) { + removeLinkedAccountAssociation(accountAssociations, changes); } else { - final SavingsAccount savingsAccount = accountAssociations.linkedSavingsAccount(); - if (savingsAccount == null || !savingsAccount.getId().equals(savingsAccountId)) { - isModified = true; - } - } - if (isModified) { - final SavingsAccount savingsAccount = this.savingsAccountAssembler.assembleFrom(savingsAccountId, backdatedTxnsAllowedTill); - this.loanApplicationValidator.validatelinkedSavingsAccount(savingsAccount, loan); + final SavingsAccount savingsAccount = this.savingsAccountRepository.findOneWithNotFoundDetection(savingsAccountId); + // If there was no previous if (accountAssociations == null) { - boolean isActive = true; - accountAssociations = AccountAssociations.associateSavingsAccount(loan, savingsAccount, - AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue(), isActive); - } else { - accountAssociations.updateLinkedSavingsAccount(savingsAccount); + createLinkedAccountAssociation(loan, savingsAccount, changes); + // When the previous one was linking to a different account + } else if (!accountAssociations.linkedSavingsAccount().getId().equals(savingsAccountId)) { + updateLinkedAccountAssociation(accountAssociations, savingsAccount, changes); } - changes.put(LoanApiConstants.linkAccountIdParameterName, savingsAccountId); - this.accountAssociationsRepository.save(accountAssociations); } } } + private void updateLinkedAccountAssociation(AccountAssociations accountAssociations, SavingsAccount savingsAccount, + Map changes) { + accountAssociations.updateLinkedSavingsAccount(savingsAccount); + this.accountAssociationsRepository.save(accountAssociations); + changes.put(LoanApiConstants.linkAccountIdParameterName, savingsAccount.getId()); + } + + private void removeLinkedAccountAssociation(final AccountAssociations accountAssociations, final Map changes) { + if (accountAssociations != null) { + this.accountAssociationsRepository.delete(accountAssociations); + changes.put(LoanApiConstants.linkAccountIdParameterName, null); + } + } + + private void createLinkedAccountAssociation(final Loan loan, final SavingsAccount savingsAccount, final Map changes) { + boolean isActive = true; + this.accountAssociationsRepository.save(AccountAssociations.associateSavingsAccount(loan, savingsAccount, + AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue(), isActive)); + changes.put(LoanApiConstants.linkAccountIdParameterName, savingsAccount.getId()); + } + private void modifyCalendar(Long loanId, Long calendarId, Loan loan, Map changes) { Calendar calendar = null; if (calendarId != null && calendarId != 0) { @@ -482,6 +493,19 @@ public CommandProcessingResult deleteApplication(final Long loanId) { .build(); } + public void validateMultiDisbursementData(final JsonCommand command, LocalDate expectedDisbursementDate) { + final String json = command.json(); + final JsonElement element = this.fromJsonHelper.parse(json); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); + final BigDecimal principal = this.fromJsonHelper.extractBigDecimalWithLocaleNamed("approvedLoanAmount", element); + loanApplicationValidator.validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + @Transactional @Override public CommandProcessingResult approveGLIMLoanAppication(final Long loanId, final JsonCommand command) { @@ -537,6 +561,8 @@ public CommandProcessingResult approveApplication(final Long loanId, final JsonC Loan loan = retrieveLoanBy(loanId); checkClientOrGroupActive(loan); + final JsonArray disbursementDataArray = command.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName); + LocalDate expectedDisbursementDate = command.localDateValueOfParameterNamed(LoanApiConstants.expectedDisbursementDateParameterName); if (expectedDisbursementDate == null) { expectedDisbursementDate = loan.getExpectedDisbursedOnLocalDate(); @@ -548,8 +574,6 @@ public CommandProcessingResult approveApplication(final Long loanId, final JsonC loanApplicationValidator.validateLoanMultiDisbursementDate(element, expectedDisbursementDate, principal); } - final JsonArray disbursementDataArray = command.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName); - boolean isSkipRepaymentOnFirstMonth = false; int numberOfDays = 0; if (loan.isSyncDisbursementWithMeeting() && (loan.isGroupLoan() || loan.isJLGLoan())) { @@ -579,7 +603,7 @@ public CommandProcessingResult approveApplication(final Long loanId, final JsonC validateTopupLoan(loan, expectedDisbursementDate); } - loan = this.loanRepository.saveAndFlush(loan); + loan = loanRepository.saveAndFlush(loan); final String noteText = command.stringValueOfParameterNamed("note"); if (StringUtils.isNotBlank(noteText)) { @@ -868,19 +892,18 @@ private void createSavingsAccountAssociation(Long savingsAccountId, Loan loan) { for (GroupSavingsIndividualMonitoringAccountData childSaving : childSavings) { gsimClientMembers.add(childSaving.getClientId()); clientAccountMappings.put(childSaving.getClientId(), childSaving.getChildAccountId()); - } if (gsimClientMembers.contains(BigDecimal.valueOf(loan.getClientId()))) { - savingsAccount = this.savingsAccountAssembler - .assembleFrom(clientAccountMappings.get(BigDecimal.valueOf(loan.getClientId())).longValue(), false); + savingsAccount = this.savingsAccountRepository + .findOneWithNotFoundDetection(clientAccountMappings.get(BigDecimal.valueOf(loan.getClientId())).longValue()); } else { throw new GroupMemberNotFoundInGSIMException(loan.getClientId()); } } else { - savingsAccount = this.savingsAccountAssembler.assembleFrom(savingsAccountId, false); + savingsAccount = this.savingsAccountRepository.findOneWithNotFoundDetection(savingsAccountId); } - this.loanApplicationValidator.validatelinkedSavingsAccount(savingsAccount, loan); + boolean isActive = true; accountAssociations = AccountAssociations.associateSavingsAccount(loan, savingsAccount, AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue(), isActive); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java index b04a547e8cc..6bf8744dd43 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java @@ -41,7 +41,6 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonQuery; import org.apache.fineract.infrastructure.core.domain.ExternalId; -import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.organisation.holiday.domain.Holiday; @@ -88,7 +87,6 @@ import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleAssembler; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService; import org.apache.fineract.portfolio.loanaccount.mapper.LoanChargeMapper; @@ -216,23 +214,6 @@ public Loan assembleFrom(final JsonCommand command) { final Boolean isFloatingInterestRate = this.fromApiJsonHelper .extractBooleanNamed(LoanApiConstants.isFloatingInterestRateParameterName, element); - // PROGRESSIVE: Repayment strategy MUST be only "advanced payment allocation" - final LoanScheduleType loanScheduleType = loanProduct.getLoanProductRelatedDetail().getLoanScheduleType(); - if (loanScheduleType.equals(LoanScheduleType.PROGRESSIVE)) { - if (!transactionProcessingStrategyCode.equals(LoanProductConstants.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)) { - throw new GeneralPlatformDomainRuleException( - "error.msg.loan.repayment.strategy.can.not.be.different.than.advanced.payment.allocation", - "Loan repayment strategy can not be different than Advanced Payment Allocation"); - } - // CUMULATIVE: Repayment strategy CANNOT be "advanced payment allocation" - } else if (loanScheduleType.equals(LoanScheduleType.CUMULATIVE)) { - if (transactionProcessingStrategyCode.equals(LoanProductConstants.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)) { - throw new GeneralPlatformDomainRuleException( - "error.msg.loan.repayment.strategy.can.not.be.equal.to.advanced.payment.allocation", - "Loan repayment strategy can not be equal to Advanced Payment Allocation"); - } - } - if (clientId != null) { client = this.clientRepository.findOneWithNotFoundDetection(clientId); } @@ -286,7 +267,8 @@ public Loan assembleFrom(final JsonCommand command) { copyAdvancedPaymentRulesIfApplicable(transactionProcessingStrategyCode, loanProduct, loanApplication); loanApplication.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory); - + // TODO: review + loanApplication.recalculateAllCharges(); topUpLoanConfiguration(element, loanApplication); return loanApplication; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementDetailsAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementDetailsAssembler.java index 19539690d81..45a510b40bc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementDetailsAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementDetailsAssembler.java @@ -6,9 +6,9 @@ * 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 @@ -20,17 +20,15 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; -import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; -import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; - import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; +import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; @RequiredArgsConstructor public class LoanDisbursementDetailsAssembler { @@ -41,37 +39,21 @@ public List fetchDisbursementData(final JsonObject comm final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(command); final String dateFormat = this.fromApiJsonHelper.extractDateFormatParameter(command); List disbursementDatas = new ArrayList<>(); - JsonArray disbursementDataArray = command.getAsJsonArray(LoanApiConstants.disbursementDataParameterName); if (disbursementDataArray != null && !disbursementDataArray.isEmpty()) { disbursementDataArray.forEach(jsonElement -> { - JsonObject jsonObject = jsonElement.getAsJsonObject(); - - LocalDate expectedDisbursementDate = jsonObject.has(LoanApiConstants.expectedDisbursementDateParameterName) - ? this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, - jsonObject, dateFormat, locale) - : null; - - BigDecimal principal = jsonObject.has(LoanApiConstants.disbursementPrincipalParameterName) - && jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).isJsonPrimitive() - && StringUtils.isNotBlank(jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).getAsString()) - ? jsonObject.getAsJsonPrimitive(LoanApiConstants.disbursementPrincipalParameterName).getAsBigDecimal() - : null; - - BigDecimal netDisbursalAmount = jsonObject.has(LoanApiConstants.disbursementNetDisbursalAmountParameterName) - && jsonObject.get(LoanApiConstants.disbursementNetDisbursalAmountParameterName).isJsonPrimitive() - && StringUtils.isNotBlank(jsonObject.get(LoanApiConstants.disbursementNetDisbursalAmountParameterName).getAsString()) - ? jsonObject.getAsJsonPrimitive(LoanApiConstants.disbursementNetDisbursalAmountParameterName).getAsBigDecimal() - : null; - - boolean isReversed = jsonObject.has(LoanApiConstants.disbursementReversedParameterName) - ? this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.disbursementReversedParameterName, jsonObject) - : false; - - disbursementDatas.add(new LoanDisbursementDetails(expectedDisbursementDate, null, principal, netDisbursalAmount, isReversed)); + LocalDate expectedDisbursementDate = this.fromApiJsonHelper + .extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, jsonElement, dateFormat, locale); + BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalNamed(LoanApiConstants.disbursementPrincipalParameterName, + jsonElement, locale); + BigDecimal netDisbursalAmount = this.fromApiJsonHelper + .extractBigDecimalNamed(LoanApiConstants.disbursementNetDisbursalAmountParameterName, jsonElement, locale); + boolean isReversed = Boolean.TRUE.equals( + this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.disbursementReversedParameterName, jsonElement)); + disbursementDatas + .add(new LoanDisbursementDetails(expectedDisbursementDate, null, principal, netDisbursalAmount, isReversed)); }); } - return disbursementDatas; } } 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 1716e909ccc..33a32d0e288 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 @@ -136,7 +136,7 @@ import org.apache.fineract.portfolio.rate.service.RateAssembler; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.service.RepaymentWithPostDatedChecksAssembler; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; import org.apache.fineract.portfolio.savings.service.GSIMReadPlatformService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; @@ -207,7 +207,7 @@ public LoanApplicationWritePlatformService loanApplicationWritePlatformService(P LoanAssembler loanAssembler, LoanSummaryWrapper loanSummaryWrapper, LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, CalendarRepository calendarRepository, CalendarInstanceRepository calendarInstanceRepository, - SavingsAccountAssembler savingsAccountAssembler, AccountAssociationsRepository accountAssociationsRepository, + SavingsAccountRepositoryWrapper savingsAccountRepository, AccountAssociationsRepository accountAssociationsRepository, LoanReadPlatformService loanReadPlatformService, BusinessEventNotifierService businessEventNotifierService, ConfigurationDomainService configurationDomainService, LoanScheduleAssembler loanScheduleAssembler, LoanUtilService loanUtilService, CalendarReadPlatformService calendarReadPlatformService, @@ -216,7 +216,7 @@ public LoanApplicationWritePlatformService loanApplicationWritePlatformService(P LoanLifecycleStateMachine defaultLoanLifecycleStateMachine) { return new LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, fromJsonHelper, loanApplicationTransitionApiJsonValidator, loanApplicationValidator, loanRepositoryWrapper, noteRepository, loanAssembler, loanSummaryWrapper, - loanRepaymentScheduleTransactionProcessorFactory, calendarRepository, calendarInstanceRepository, savingsAccountAssembler, + loanRepaymentScheduleTransactionProcessorFactory, calendarRepository, calendarInstanceRepository, savingsAccountRepository, accountAssociationsRepository, loanReadPlatformService, businessEventNotifierService, configurationDomainService, loanScheduleAssembler, loanUtilService, calendarReadPlatformService, entityDatatableChecksWritePlatformService, glimRepository, loanRepository, gsimReadPlatformService, defaultLoanLifecycleStateMachine); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index f08c25ce5c3..0ea2c32be52 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -72,6 +72,7 @@ import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.EarlyPaymentLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; @@ -2079,8 +2080,12 @@ public void uc107() { overpaymentAccount); assertNotNull(localLoanProductId); + Integer localLoanProductId2 = createLoanProduct("500", "15", "4", false, LoanScheduleType.CUMULATIVE, assetAccount, + incomeAccount, expenseAccount, overpaymentAccount); + assertNotNull(localLoanProductId2); + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, - () -> applyForLoanApplication(client.getClientId(), localLoanProductId, BigDecimal.valueOf(500.0), 45, 15, 3, + () -> applyForLoanApplication(client.getClientId(), localLoanProductId2, BigDecimal.valueOf(500.0), 45, 15, 3, BigDecimal.ZERO, "01 January 2023", "01 January 2023", FineractStyleLoanRepaymentScheduleTransactionProcessor.STRATEGY_CODE, LoanScheduleProcessingType.VERTICAL.name())); @@ -4396,6 +4401,21 @@ private static Integer createLoanProduct(final String principal, final String re return loanTransactionHelper.getLoanProductId(loanProductJSON); } + private static Integer createLoanProduct(final String principal, final String repaymentAfterEvery, final String numberOfRepayments, + boolean autoPayForDownPayment, LoanScheduleType loanScheduleType, final Account... accounts) { + LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------"); + final String loanProductJSON = new LoanProductTestBuilder().withMinPrincipal(principal).withPrincipal(principal) + .withRepaymentTypeAsDays().withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments) + .withEnableDownPayment(true, "25", autoPayForDownPayment).withinterestRatePerPeriod("0") + .withInterestRateFrequencyTypeAsMonths() + .withRepaymentStrategy(EarlyPaymentLoanRepaymentScheduleTransactionProcessor.STRATEGY_CODE) + .withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts) + .withInterestCalculationPeriodTypeAsRepaymentPeriod(true).withInterestTypeAsDecliningBalance().withMultiDisburse() + .withDisallowExpectedDisbursements(true).withLoanScheduleType(loanScheduleType).withDaysInMonth("30").withDaysInYear("365") + .withMoratorium("0", "0").build(null); + return loanTransactionHelper.getLoanProductId(loanProductJSON); + } + private static ArrayList> createLoanProductGetError(final String principal, final String repaymentAfterEvery, final String numberOfRepayments, boolean autoPayForDownPayment, LoanScheduleType loanScheduleType, LoanScheduleProcessingType loanScheduleProcessingType, AdvancedPaymentData allocationRuleData, final Account... accounts) { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java index 3618bd14df4..c7150d369ee 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java @@ -7479,12 +7479,16 @@ private HashMap copyForUpdate(HashMap charge, String amount) { map.put("amount", amount); } if (charge.get("dueDate") != null) { - map.put("dueDate", charge.get("dueDate")); + map.put("dueDate", DATE_TIME_FORMATTER.format(fromArrayToLocalDate((List) charge.get("dueDate")))); } map.put("chargeId", charge.get("chargeId")); return map; } + private LocalDate fromArrayToLocalDate(List dueDate) { + return LocalDate.of(dueDate.get(0), dueDate.get(1), dueDate.get(2)); + } + private HashMap createTrancheDetail(final String date, final String amount) { HashMap detail = new HashMap(); detail.put("expectedDisbursementDate", date); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccrualTransactionWithInterestAndChargeAccrualDateAsSubmittedOnDateTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccrualTransactionWithInterestAndChargeAccrualDateAsSubmittedOnDateTest.java new file mode 100644 index 00000000000..87d6a87aaeb --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccrualTransactionWithInterestAndChargeAccrualDateAsSubmittedOnDateTest.java @@ -0,0 +1,246 @@ +/** + * 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.integrationtests; + +import static org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY; + +import java.math.BigDecimal; +import org.apache.fineract.client.models.PostLoanProductsRequest; +import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdResponse; +import org.apache.fineract.client.models.PostLoansRequest; +import org.apache.fineract.client.models.PostLoansResponse; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; +import org.apache.fineract.integrationtests.common.SchedulerJobHelper; +import org.junit.jupiter.api.Test; + +public class LoanAccrualTransactionWithInterestAndChargeAccrualDateAsSubmittedOnDateTest extends BaseLoanIntegrationTest { + + private SchedulerJobHelper schedulerJobHelper = new SchedulerJobHelper(this.requestSpec); + + @Test + public void accrualTransactionForInterestBearingLoan_WithoutCharges_SubmittedOnDateAsChargeAccrualDateWorksTest() { + runAt("15 April 2024", () -> { + + try { + // Configure Charge accrual date as submitted on date + GlobalConfigurationHelper.updateChargeAccrualDateConfiguration(this.requestSpec, this.responseSpec, "submitted-date"); + + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + // Create Loan Product + PostLoanProductsRequest loanProductsRequest = createLoanProductWithInterestCalculation(); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest); + + // Apply and Approve Loan + Long loanId = applyAndApproveLoanApplication(clientId, loanProductResponse.getResourceId(), "15 April 2024", 1000.0, 4); + + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(500), "15 April 2024"); + + // Verify Repayment Schedule and Due Dates + verifyRepaymentSchedule(loanId, // + installment(500.0, null, "15 April 2024"), // + installment(114.41, 29.59, 0.0, 0.0, 144.0, false, "30 April 2024"), // + installment(121.18, 22.82, 0.0, 0.0, 144.0, false, "15 May 2024"), // + installment(128.35, 15.65, 0.0, 0.0, 144.0, false, "30 May 2024"), // + installment(136.06, 8.05, 0.0, 0.0, 144.11, false, "14 June 2024") // + ); + + verifyTransactions(loanId, // + transaction(500.0, "Disbursement", "15 April 2024", 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); + + // update business date + updateBusinessDate("25 April 2024"); + + // run cob + schedulerJobHelper.executeAndAwaitJob("Loan COB"); + + verifyTransactions(loanId, // + transaction(500.0, "Disbursement", "15 April 2024", 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(17.75, "Accrual", "24 April 2024", 0.0, 0.0, 17.75, 0.0, 0.0, 0.0, 0.0) // + ); + + // update business date + updateBusinessDate("26 April 2024"); + + // disburse amount + disburseLoan(loanId, BigDecimal.valueOf(500), "26 April 2024"); + + // Verify Repayment Schedule and Due Dates + verifyRepaymentSchedule(loanId, // + installment(500.0, null, "15 April 2024"), // + installment(500.0, null, "26 April 2024"), // + installment(250.52, 37.48, 0.0, 0.0, 288.0, false, "30 April 2024"), // + installment(243.65, 44.35, 0.0, 0.0, 288.0, false, "15 May 2024"), // + installment(258.07, 29.93, 0.0, 0.0, 288.0, false, "30 May 2024"), // + installment(247.76, 14.66, 0.0, 0.0, 262.42, false, "14 June 2024") // + ); + + verifyTransactions(loanId, // + transaction(500.0, "Disbursement", "15 April 2024", 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(22.49, "Accrual", "24 April 2024", 0.0, 0.0, 22.49, 0.0, 0.0, 0.0, 0.0), // + transaction(500.0, "Disbursement", "26 April 2024", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)); + + } finally { + GlobalConfigurationHelper.updateChargeAccrualDateConfiguration(this.requestSpec, this.responseSpec, "due-date"); + } + + }); + + } + + @Test + public void accrualTransactionForInterestBearingLoan_WithCharges_SubmittedOnDateAsChargeAccrualDateWorksTest() { + runAt("15 April 2024", () -> { + + try { + // Configure Charge accrual date as submitted on date + GlobalConfigurationHelper.updateChargeAccrualDateConfiguration(this.requestSpec, this.responseSpec, "submitted-date"); + + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + // Create Loan Product + PostLoanProductsRequest loanProductsRequest = createLoanProductWithInterestCalculation(); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest); + + // Apply and Approve Loan + Long loanId = applyAndApproveLoanApplication(clientId, loanProductResponse.getResourceId(), "15 April 2024", 1000.0, 4); + + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(500), "15 April 2024"); + + // Verify Repayment Schedule and Due Dates + verifyRepaymentSchedule(loanId, // + installment(500.0, null, "15 April 2024"), // + installment(114.41, 29.59, 0.0, 0.0, 144.0, false, "30 April 2024"), // + installment(121.18, 22.82, 0.0, 0.0, 144.0, false, "15 May 2024"), // + installment(128.35, 15.65, 0.0, 0.0, 144.0, false, "30 May 2024"), // + installment(136.06, 8.05, 0.0, 0.0, 144.11, false, "14 June 2024") // + ); + + verifyTransactions(loanId, // + transaction(500.0, "Disbursement", "15 April 2024", 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); + + // update business date + updateBusinessDate("24 April 2024"); + + // add charge + addCharge(loanId, false, 10.0, "29 April 2024"); + + // Verify Repayment Schedule and Due Dates + verifyRepaymentSchedule(loanId, // + installment(500.0, null, "15 April 2024"), // + installment(114.41, 29.59, 10.0, 0.0, 154.0, false, "30 April 2024"), // + installment(121.18, 22.82, 0.0, 0.0, 144.0, false, "15 May 2024"), // + installment(128.35, 15.65, 0.0, 0.0, 144.0, false, "30 May 2024"), // + installment(136.06, 8.05, 0.0, 0.0, 144.11, false, "14 June 2024") // + ); + + // update business date + updateBusinessDate("25 April 2024"); + + // run cob + schedulerJobHelper.executeAndAwaitJob("Loan COB"); + + verifyTransactions(loanId, // + transaction(500.0, "Disbursement", "15 April 2024", 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(27.75, "Accrual", "24 April 2024", 0.0, 0.0, 17.75, 10.0, 0.0, 0.0, 0.0) // + ); + + // update business date + updateBusinessDate("26 April 2024"); + + // disburse amount + disburseLoan(loanId, BigDecimal.valueOf(500), "26 April 2024"); + + // Verify Repayment Schedule and Due Dates + verifyRepaymentSchedule(loanId, // + installment(500.0, null, "15 April 2024"), // + installment(500.0, null, "26 April 2024"), // + installment(250.52, 37.48, 10.0, 0.0, 298.0, false, "30 April 2024"), // + installment(243.65, 44.35, 0.0, 0.0, 288.0, false, "15 May 2024"), // + installment(258.07, 29.93, 0.0, 0.0, 288.0, false, "30 May 2024"), // + installment(247.76, 14.66, 0.0, 0.0, 262.42, false, "14 June 2024") // + ); + + verifyTransactions(loanId, // + transaction(500.0, "Disbursement", "15 April 2024", 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(32.49, "Accrual", "24 April 2024", 0.0, 0.0, 22.49, 10.0, 0.0, 0.0, 0.0), // + transaction(500.0, "Disbursement", "26 April 2024", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)); + + // run cob + schedulerJobHelper.executeAndAwaitJob("Loan COB"); + + verifyTransactions(loanId, // + transaction(500.0, "Disbursement", "15 April 2024", 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(32.49, "Accrual", "24 April 2024", 0.0, 0.0, 22.49, 10.0, 0.0, 0.0, 0.0), // + transaction(2.50, "Accrual", "25 April 2024", 0.0, 0.0, 2.50, 0.0, 0.0, 0.0, 0.0), // + transaction(500.0, "Disbursement", "26 April 2024", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)); + + } finally { + GlobalConfigurationHelper.updateChargeAccrualDateConfiguration(this.requestSpec, this.responseSpec, "due-date"); + } + + }); + + } + + private Long applyAndApproveLoanApplication(Long clientId, Long productId, String disbursementDate, double amount, + int numberOfRepayments) { + PostLoansRequest postLoansRequest = new PostLoansRequest().clientId(clientId).productId(productId) + .expectedDisbursementDate(disbursementDate).dateFormat(DATETIME_PATTERN) + .transactionProcessingStrategyCode(DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY) + .locale("en").submittedOnDate(disbursementDate).amortizationType(AmortizationType.EQUAL_INSTALLMENTS) + .interestRatePerPeriod(new BigDecimal(12.0)) + .interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD) + .interestType(InterestType.DECLINING_BALANCE).repaymentEvery(15).repaymentFrequencyType(RepaymentFrequencyType.DAYS) + .numberOfRepayments(numberOfRepayments).loanTermFrequency(numberOfRepayments * 15).loanTermFrequencyType(0) + .maxOutstandingLoanBalance(BigDecimal.valueOf(amount)).principal(BigDecimal.valueOf(amount)).loanType("individual"); + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(postLoansRequest); + PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), + approveLoanRequest(amount, disbursementDate)); + return approvedLoanResult.getLoanId(); + } + + private PostLoanProductsRequest createLoanProductWithInterestCalculation() { + return createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct().multiDisburseLoan(true)// + .disallowExpectedDisbursements(true)// + .allowApprovedDisbursedAmountsOverApplied(false)// + .overAppliedCalculationType(null)// + .overAppliedNumber(null)// + .principal(1000.0)// + .numberOfRepayments(4)// + .repaymentEvery(15)// + .repaymentFrequencyType(RepaymentFrequencyType.DAYS.longValue())// + .interestType(InterestType.DECLINING_BALANCE)// + .amortizationType(AmortizationType.EQUAL_INSTALLMENTS)// + .interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)// + .interestRatePerPeriod(12.0) // + .interestRateFrequencyType(InterestRateFrequencyType.MONTHS)// + .isInterestRecalculationEnabled(true) // + .interestRecalculationCompoundingMethod(0).rescheduleStrategyMethod(3).recalculationRestFrequencyType(1) + .recalculationRestFrequencyInterval(1); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanApplicationApprovalTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanApplicationApprovalTest.java index 7a667bb7713..6b37a304346 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanApplicationApprovalTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanApplicationApprovalTest.java @@ -155,7 +155,7 @@ public void loanApplicationShouldFailIfTransactionProcessingStrategyIsAdvancedPa log.info("---------------------------------CLIENT CREATED WITH ID--------------------------------------------------- {}", clientId); final Integer loanProductId = this.loanTransactionHelper - .getLoanProductId(new LoanProductTestBuilder().withInterestTypeAsDecliningBalance().withTranches(true) + .getLoanProductId(new LoanProductTestBuilder().withInterestTypeAsDecliningBalance().withTranches(false) .withInterestCalculationPeriodTypeAsRepaymentPeriod(true).build(null)); log.info("----------------------------------LOAN PRODUCT CREATED WITH ID------------------------------------------- {}", loanProductId);