diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java index ad28f696c2b..e468e4a17c5 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java @@ -159,8 +159,7 @@ public static CommandSource fullEntryFrom(final CommandWrapper wrapper, final Js .transactionId(command.getTransactionId()) // .creditBureauId(command.getCreditBureauId()) // .organisationCreditBureauId(command.getOrganisationCreditBureauId()) // - .loanExternalId(command.getLoanExternalId()) // - .build(); // + .loanExternalId(command.getLoanExternalId()).build(); // } public String getPermissionCode() { diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandWrapper.java b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandWrapper.java index 5b51f40de3a..fd73b43b9c8 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandWrapper.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandWrapper.java @@ -251,8 +251,16 @@ public boolean isInterestPauseResource() { return this.entityName.equalsIgnoreCase("INTEREST_PAUSE"); } - public boolean isInterestPauseExternalIdResource() { - return this.entityName.equalsIgnoreCase("INTEREST_PAUSE") && this.href.contains("/external-id/"); + public boolean isInterestPauseCreateResource() { + return this.entityName.equalsIgnoreCase("INTEREST_PAUSE") && "CREATE".equalsIgnoreCase(this.actionName); + } + + public boolean isInterestPauseUpdateResource() { + return this.entityName.equalsIgnoreCase("INTEREST_PAUSE") && "UPDATE".equalsIgnoreCase(this.actionName); + } + + public boolean isInterestPauseDeleteResource() { + return this.entityName.equalsIgnoreCase("INTEREST_PAUSE") && "DELETE".equalsIgnoreCase(this.actionName); } public Long commandId() { diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 497862cbffe..740fbbbc67a 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -3732,4 +3732,22 @@ public CommandWrapperBuilder createInterestPauseByExternalId(final String loanEx this.href = "/v1/loans/external-id/" + loanExternalId + "/interest-pauses"; return this; } + + public CommandWrapperBuilder deleteInterestPause(final long loanId, final long variationId) { + this.actionName = "DELETE"; + this.entityName = "INTEREST_PAUSE"; + this.loanId = loanId; + this.entityId = variationId; + this.href = "/v1/loans/" + loanId + "/interest-pauses/" + variationId; + return this; + } + + public CommandWrapperBuilder updateInterestPause(final long loanId, final long variationId) { + this.actionName = "UPDATE"; + this.entityName = "INTEREST_PAUSE"; + this.loanId = loanId; + this.entityId = variationId; + this.href = "/v1/loans/" + loanId + "/interest-pauses/" + variationId; + return this; + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java index 9b525d5cd9c..f021cb23cae 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java @@ -249,8 +249,16 @@ private NewCommandSourceHandler findCommandHandler(final CommandWrapper wrapper) } else { throw new UnsupportedCommandException(wrapper.commandName()); } - } else if (wrapper.isInterestPauseResource() || wrapper.isInterestPauseExternalIdResource()) { - handler = applicationContext.getBean("interestPauseCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isInterestPauseResource()) { + if (wrapper.isInterestPauseCreateResource()) { + handler = applicationContext.getBean("createInterestPauseCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isInterestPauseUpdateResource()) { + handler = applicationContext.getBean("updateInterestPauseCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isInterestPauseDeleteResource()) { + handler = applicationContext.getBean("deleteInterestPauseCommandHandler", NewCommandSourceHandler.class); + } else { + throw new UnsupportedCommandException(wrapper.commandName()); + } } else { handler = commandHandlerProvider.getHandler(wrapper.entityName(), wrapper.actionName()); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/api/LoanInterestPauseApiResource.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/api/LoanInterestPauseApiResource.java index 1b08ad6d8d9..253727547fd 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/api/LoanInterestPauseApiResource.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/api/LoanInterestPauseApiResource.java @@ -25,12 +25,15 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.domain.CommandWrapper; @@ -50,6 +53,7 @@ public class LoanInterestPauseApiResource { private static final String RESOURCE_NAME_FOR_PERMISSIONS = "LOAN"; + private static final String MODIFY_RESOURCE_NAME_FOR_PERMISSIONS = "UPDATE LOAN"; private final PlatformSecurityContext context; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; @@ -64,7 +68,7 @@ public class LoanInterestPauseApiResource { public CommandProcessingResult createInterestPause(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, @RequestBody(required = true) final InterestPauseRequestDto request) { - this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); + this.context.authenticatedUser().validateHasReadPermission(MODIFY_RESOURCE_NAME_FOR_PERMISSIONS); final CommandWrapper commandRequest = new CommandWrapperBuilder().createInterestPause(loanId).withJson(request.toJson()).build(); @@ -81,7 +85,7 @@ public CommandProcessingResult createInterestPauseByExternalId( @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId, @RequestBody(required = true) final InterestPauseRequestDto request) { - this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); + this.context.authenticatedUser().validateHasReadPermission(MODIFY_RESOURCE_NAME_FOR_PERMISSIONS); final CommandWrapper commandRequest = new CommandWrapperBuilder().createInterestPauseByExternalId(loanExternalId) .withJson(request.toJson()).build(); @@ -114,4 +118,38 @@ public List retrieveInterestPausesByExternalId( return this.interestPauseReadPlatformService.retrieveInterestPauses(loanExternalId); } + + @DELETE + @Path("/{loanId}/interest-pauses/{variationId}") + @Operation(summary = "Delete an interest pause period", description = "Deletes a specific interest pause period by its variation ID.") + @ApiResponses({ @ApiResponse(responseCode = "204", description = "No Content") }) + public Response deleteInterestPause(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + @PathParam("variationId") @Parameter(description = "variationId") final Long variationId) { + + this.context.authenticatedUser().validateHasReadPermission(MODIFY_RESOURCE_NAME_FOR_PERMISSIONS); + + final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteInterestPause(loanId, variationId).build(); + + this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return Response.noContent().build(); + } + + @PUT + @Path("/{loanId}/interest-pauses/{variationId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Update an interest pause period", description = "Updates a specific interest pause period by its variation ID.") + @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") }) + public CommandProcessingResult updateInterestPause(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + @PathParam("variationId") @Parameter(description = "variationId") final Long variationId, + @RequestBody(required = true) final InterestPauseRequestDto request) { + + this.context.authenticatedUser().validateHasReadPermission(MODIFY_RESOURCE_NAME_FOR_PERMISSIONS); + + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateInterestPause(loanId, variationId) + .withJson(request.toJson()).build(); + + return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/handler/InterestPauseCommandHandler.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/handler/CreateInterestPauseCommandHandler.java similarity index 85% rename from fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/handler/InterestPauseCommandHandler.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/handler/CreateInterestPauseCommandHandler.java index 5747625dd22..6246f278c8b 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/handler/InterestPauseCommandHandler.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/handler/CreateInterestPauseCommandHandler.java @@ -27,16 +27,14 @@ import org.apache.fineract.portfolio.interestpauses.service.InterestPauseWritePlatformService; import org.springframework.stereotype.Component; -@Component("interestPauseCommandHandler") +@Component("createInterestPauseCommandHandler") @RequiredArgsConstructor -public class InterestPauseCommandHandler implements NewCommandSourceHandler { +public class CreateInterestPauseCommandHandler implements NewCommandSourceHandler { private final InterestPauseWritePlatformService interestPauseService; @Override public CommandProcessingResult processCommand(final JsonCommand command) { - CommandProcessingResult result; - final String startDate = command.stringValueOfParameterNamed("startDate"); final String endDate = command.stringValueOfParameterNamed("endDate"); final String dateFormat = command.stringValueOfParameterNamed("dateFormat"); @@ -44,15 +42,13 @@ public CommandProcessingResult processCommand(final JsonCommand command) { if (command.getLoanId() != null) { final Long loanId = command.getLoanId(); - result = interestPauseService.createInterestPause(loanId, startDate, endDate, dateFormat, locale); + return interestPauseService.createInterestPause(loanId, startDate, endDate, dateFormat, locale); } else if (command.getLoanExternalId() != null) { final ExternalId loanExternalId = command.getLoanExternalId(); - result = interestPauseService.createInterestPause(loanExternalId, startDate, endDate, dateFormat, locale); + return interestPauseService.createInterestPause(loanExternalId, startDate, endDate, dateFormat, locale); } else { throw new PlatformApiDataValidationException("validation.msg.missing.loan.id.or.external.id", "Either loanId or loanExternalId must be provided.", "loanId"); } - - return result; } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/handler/DeleteInterestPauseCommandHandler.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/handler/DeleteInterestPauseCommandHandler.java new file mode 100644 index 00000000000..8068c8c5a28 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/handler/DeleteInterestPauseCommandHandler.java @@ -0,0 +1,41 @@ +/** + * 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.interestpauses.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.interestpauses.service.InterestPauseWritePlatformService; +import org.springframework.stereotype.Component; + +@Component("deleteInterestPauseCommandHandler") +@RequiredArgsConstructor +public class DeleteInterestPauseCommandHandler implements NewCommandSourceHandler { + + private final InterestPauseWritePlatformService interestPauseService; + + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + final Long loanId = command.getLoanId(); + final Long termVariationId = command.getResourceId(); + + return interestPauseService.deleteInterestPause(loanId, termVariationId); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/handler/UpdateInterestPauseCommandHandler.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/handler/UpdateInterestPauseCommandHandler.java new file mode 100644 index 00000000000..04a9b2f03df --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/handler/UpdateInterestPauseCommandHandler.java @@ -0,0 +1,45 @@ +/** + * 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.interestpauses.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.interestpauses.service.InterestPauseWritePlatformService; +import org.springframework.stereotype.Component; + +@Component("updateInterestPauseCommandHandler") +@RequiredArgsConstructor +public class UpdateInterestPauseCommandHandler implements NewCommandSourceHandler { + + private final InterestPauseWritePlatformService interestPauseService; + + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + final Long loanId = command.getLoanId(); + final Long termVariationId = command.getResourceId(); + final String startDate = command.stringValueOfParameterNamed("startDate"); + final String endDate = command.stringValueOfParameterNamed("endDate"); + final String dateFormat = command.stringValueOfParameterNamed("dateFormat"); + final String locale = command.stringValueOfParameterNamed("locale"); + + return interestPauseService.updateInterestPause(loanId, termVariationId, startDate, endDate, dateFormat, locale); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformService.java index e3b8c397a22..491a45171dc 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformService.java @@ -23,6 +23,21 @@ public interface InterestPauseWritePlatformService { + /** + * Create a new interest pause period for a loan identified by its external ID. + * + * @param loanExternalId + * the external ID of the loan + * @param startDate + * the start date of the interest pause period (inclusive) + * @param endDate + * the end date of the interest pause period (inclusive) + * @param dateFormat + * the format of the provided dates + * @param locale + * the locale used for date parsing + * @return the ID of the created loan term variation representing the interest pause + */ CommandProcessingResult createInterestPause(ExternalId loanExternalId, String startDate, String endDate, String dateFormat, String locale); @@ -35,7 +50,42 @@ CommandProcessingResult createInterestPause(ExternalId loanExternalId, String st * the start date of the interest pause period (inclusive) * @param endDate * the end date of the interest pause period (inclusive) + * @param dateFormat + * the format of the provided dates + * @param locale + * the locale used for date parsing * @return the ID of the created loan term variation representing the interest pause */ CommandProcessingResult createInterestPause(Long loanId, String startDate, String endDate, String dateFormat, String locale); + + /** + * Delete an existing interest pause period for a loan. + * + * @param loanId + * the ID of the loan + * @param variationId + * the ID of the loan term variation representing the interest pause + * @return the result of the delete operation + */ + CommandProcessingResult deleteInterestPause(Long loanId, Long variationId); + + /** + * Update an existing interest pause period for a loan identified by its internal ID. + * + * @param loanId + * the ID of the loan + * @param variationId + * the ID of the loan term variation representing the interest pause to be updated + * @param startDateString + * the new start date of the interest pause period (inclusive) as a string + * @param endDateString + * the new end date of the interest pause period (inclusive) as a string + * @param dateFormat + * the format of the provided dates (e.g., "yyyy-MM-dd") + * @param locale + * the locale used for parsing the provided dates + * @return the updated loan term variation ID along with the updated fields + */ + CommandProcessingResult updateInterestPause(Long loanId, Long variationId, String startDateString, String endDateString, + String dateFormat, String locale); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformServiceImpl.java index 272ba3f9ff6..095ec4024d9 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformServiceImpl.java @@ -18,12 +18,19 @@ */ package org.apache.fineract.portfolio.interestpauses.service; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanStatus.ACTIVE; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType.INTEREST_PAUSE; +import static org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType.PROGRESSIVE; + import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Objects; import java.util.function.Consumer; import java.util.function.Supplier; import lombok.AllArgsConstructor; @@ -34,11 +41,13 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermVariationsRepository; +import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.transaction.annotation.Transactional; @AllArgsConstructor @@ -47,6 +56,7 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite private final LoanTermVariationsRepository loanTermVariationsRepository; private final LoanRepositoryWrapper loanRepositoryWrapper; + private final PlatformSecurityContext context; @Override public CommandProcessingResult createInterestPause(ExternalId loanExternalId, String startDateString, String endDateString, @@ -68,14 +78,59 @@ public CommandProcessingResult createInterestPause(Long loanId, String startDate locale); } + @Override + public CommandProcessingResult deleteInterestPause(Long loanId, Long variationId) { + LoanTermVariations variation = loanTermVariationsRepository + .findByIdAndLoanIdAndTermType(variationId, loanId, INTEREST_PAUSE.getValue()) + .orElseThrow(() -> new GeneralPlatformDomainRuleException("error.msg.variation.not.found", + "Variation not found for the given loan ID")); + + loanTermVariationsRepository.delete(variation); + + return new CommandProcessingResultBuilder().withEntityId(variationId).build(); + } + + @Override + public CommandProcessingResult updateInterestPause(Long loanId, Long variationId, String startDateString, String endDateString, + String dateFormat, String locale) { + Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); + + LocalDate startDate = parseDate(startDateString, dateFormat, locale); + LocalDate endDate = parseDate(endDateString, dateFormat, locale); + + validateInterestPauseDates(loan, startDate, endDate, dateFormat, locale); + + LoanTermVariations variation = loanTermVariationsRepository + .findByIdAndLoanIdAndTermType(variationId, loanId, INTEREST_PAUSE.getValue()) + .orElseThrow(() -> new GeneralPlatformDomainRuleException("error.msg.variation.not.found", + "Variation not found for the given loan ID")); + + validateVariations(loan, variation); + + variation.setTermApplicableFrom(startDate); + variation.setDateValue(endDate); + + AppUser currentUser = context.authenticatedUser(); + variation.setUpdatedBy(currentUser != null ? currentUser.getId() : null); + variation.setUpdatedOnDate(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant())); + + LoanTermVariations updatedVariation = loanTermVariationsRepository.save(variation); + + return new CommandProcessingResultBuilder().withEntityId(updatedVariation.getId()) + .with(Map.of("startDate", startDate.toString(), "endDate", endDate.toString())).build(); + } + private CommandProcessingResult processInterestPause(Supplier loanSupplier, LocalDate startDate, LocalDate endDate, String dateFormat, String locale) { final Loan loan = loanSupplier.get(); validateInterestPauseDates(loan, startDate, endDate, dateFormat, locale); - LoanTermVariations variation = new LoanTermVariations(LoanTermVariationType.INTEREST_PAUSE.getValue(), startDate, null, endDate, - false, loan); + LoanTermVariations variation = new LoanTermVariations(INTEREST_PAUSE.getValue(), startDate, null, endDate, false, loan); + + AppUser currentUser = context.authenticatedUser(); + variation.setCreatedBy(currentUser != null ? currentUser.getId() : null); + variation.setCreatedOnDate(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant())); LoanTermVariations savedVariation = loanTermVariationsRepository.saveAndFlush(variation); @@ -109,6 +164,36 @@ private void validateInterestPauseDates(Loan loan, LocalDate startDate, LocalDat .format("Interest pause end date (%s) must not be before the interest pause start date (%s).", endDate, startDate), endDate, startDate); } + + if (!Objects.equals(loan.getLoanStatus(), ACTIVE.getValue())) { + throw new GeneralPlatformDomainRuleException("loan.must.be.active", + "Operations on interest pauses are restricted to active loans."); + } + + if (!PROGRESSIVE.equals(loan.getLoanRepaymentScheduleDetail().getLoanScheduleType())) { + throw new GeneralPlatformDomainRuleException("loan.must.be.progressive", + "Interest pause is only supported for progressive loans."); + } + + if (!loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { + throw new GeneralPlatformDomainRuleException("loan.must.have.recalculate.interest.enabled", + "Interest pause is only supported for loans with recalculate interest enabled."); + } + } + + private void validateVariations(Loan loan, LoanTermVariations variation) { + if (variation == null) { + throw new GeneralPlatformDomainRuleException("interest.pause.not.found", + "The specified interest pause does not exist for the given loan."); + } + + List existingVariations = loan.getLoanTermVariations(); + for (LoanTermVariations existingVariation : existingVariations) { + if (!existingVariation.equals(variation) && variation.getTermApplicableFrom().isBefore(existingVariation.getDateValue()) + && variation.getDateValue().isAfter(existingVariation.getTermApplicableFrom())) { + throw new GeneralPlatformDomainRuleException("interest.pause.overlapping", "Overlapping interest pauses are not allowed."); + } + } } private LocalDate parseDate(String date, String dateFormat, String locale) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariations.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariations.java index d5f41828db0..891dde0e2c7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariations.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariations.java @@ -27,6 +27,7 @@ import jakarta.persistence.Table; import java.math.BigDecimal; import java.time.LocalDate; +import java.time.LocalDateTime; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; @@ -61,6 +62,18 @@ public class LoanTermVariations extends AbstractPersistableCustom { @Column(name = "is_active", nullable = false) private Boolean isActive; + @Column(name = "created_by") + private Long createdBy; + + @Column(name = "created_on_date") + private LocalDateTime createdOnDate; + + @Column(name = "updated_by") + private Long updatedBy; + + @Column(name = "updated_on_date") + private LocalDateTime updatedOnDate; + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") private LoanTermVariations parent; @@ -139,6 +152,10 @@ public LocalDate fetchDateValue() { return this.dateValue; } + public void setDateValue(LocalDate dateValue) { + this.dateValue = dateValue; + } + public void setTermApplicableFrom(LocalDate termApplicableFrom) { this.termApplicableFrom = termApplicableFrom; } @@ -167,4 +184,35 @@ public void markAsInactive() { this.isActive = false; } + public Long getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(Long createdBy) { + this.createdBy = createdBy; + } + + public LocalDateTime getCreatedOnDate() { + return createdOnDate; + } + + public void setCreatedOnDate(LocalDateTime createdOnDate) { + this.createdOnDate = createdOnDate; + } + + public Long getUpdatedBy() { + return updatedBy; + } + + public void setUpdatedBy(Long updatedBy) { + this.updatedBy = updatedBy; + } + + public LocalDateTime getUpdatedOnDate() { + return updatedOnDate; + } + + public void setUpdatedOnDate(LocalDateTime updatedOnDate) { + this.updatedOnDate = updatedOnDate; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanTermVariationsRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanTermVariationsRepository.java index 0bf7209fff8..57993fe05e0 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanTermVariationsRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanTermVariationsRepository.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain; import java.util.List; +import java.util.Optional; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; @@ -60,4 +61,12 @@ public interface LoanTermVariationsRepository """) List findLoanTermVariationsByExternalLoanIdAndTermType(@Param("loanExternalId") ExternalId loanExternalId, @Param("termType") int termType); + + @Query(""" + select ltv + from LoanTermVariations ltv + where ltv.id = :variationId and ltv.loan.id = :loanId and ltv.termType = :termType + """) + Optional findByIdAndLoanIdAndTermType(@Param("variationId") long variationId, @Param("loanId") long loanId, + @Param("termType") int termType); } 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 b7b9a955dfc..a1a1fddbbba 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 @@ -471,7 +471,7 @@ public InterestPauseReadPlatformService interestPauseReadPlatformService(LoanTer @Bean @ConditionalOnMissingBean(InterestPauseWritePlatformService.class) public InterestPauseWritePlatformService interestPauseWritePlatformService(LoanTermVariationsRepository loanTermVariationsRepository, - LoanRepositoryWrapper loanRepositoryWrapper) { - return new InterestPauseWritePlatformServiceImpl(loanTermVariationsRepository, loanRepositoryWrapper); + LoanRepositoryWrapper loanRepositoryWrapper, PlatformSecurityContext context) { + return new InterestPauseWritePlatformServiceImpl(loanTermVariationsRepository, loanRepositoryWrapper, context); } } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 673a92f1fc7..b058f2a2a20 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -180,4 +180,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0162_add_additional_audit_fields_to_term_variations.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0162_add_additional_audit_fields_to_term_variations.xml new file mode 100644 index 00000000000..fbb1ef2bf64 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0162_add_additional_audit_fields_to_term_variations.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestPauseApiTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestPauseApiTest.java index 347b72f2369..d4f5d0dc0c1 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestPauseApiTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestPauseApiTest.java @@ -24,38 +24,30 @@ import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.math.BigDecimal; -import java.util.Arrays; -import java.util.List; import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.client.models.AdvancedPaymentData; -import org.apache.fineract.client.models.PaymentAllocationOrder; -import org.apache.fineract.client.models.PostClientsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; -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.Utils; import org.apache.fineract.integrationtests.common.accounting.Account; import org.apache.fineract.integrationtests.common.accounting.AccountHelper; +import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; 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.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; -import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Slf4j -@ExtendWith({ LoanTestLifecycleExtension.class }) +@ExtendWith(LoanTestLifecycleExtension.class) public class LoanInterestPauseApiTest extends BaseLoanIntegrationTest { private static final Logger LOG = LoggerFactory.getLogger(LoanInterestPauseApiTest.class); @@ -63,52 +55,56 @@ public class LoanInterestPauseApiTest extends BaseLoanIntegrationTest { private static RequestSpecification REQUEST_SPEC; private static ResponseSpecification RESPONSE_SPEC; private static ResponseSpecification RESPONSE_SPEC_403; + private static ResponseSpecification RESPONSE_SPEC_204; private static LoanTransactionHelper LOAN_TRANSACTIONAL_HELPER; + private static LoanTransactionHelper LOAN_TRANSACTIONAL_HELPER_204; private static LoanTransactionHelper LOAN_TRANSACTION_HELPER_403; - private static AccountHelper accountHelper; - private static PostClientsResponse client; - private static Integer loanProductId; - private static Long loanId; - private static Long nonExistLoanId = 99999L; + private static AccountHelper ACCOUNT_HELPER; + private static final Integer nonExistLoanId = 99999; private static String externalId; - private static String nonExistExternalId = "7c4fb86f-a778-4d02-b7a8-ec3ec98941fa"; - - @BeforeAll - public static void setupTests() { + private static final String nonExistExternalId = "7c4fb86f-a778-4d02-b7a8-ec3ec98941fa"; + private Integer clientId; + private Integer loanProductId; + private Integer loanId; + private final String loanPrincipalAmount = "10000.00"; + private final String numberOfRepayments = "12"; + private final String interestRatePerPeriod = "18"; + private final String dateString = "01 January 2023"; + + @BeforeEach + public void initialize() { Utils.initializeRESTAssured(); REQUEST_SPEC = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); REQUEST_SPEC.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); RESPONSE_SPEC = new ResponseSpecBuilder().expectStatusCode(200).build(); RESPONSE_SPEC_403 = new ResponseSpecBuilder().expectStatusCode(403).build(); + RESPONSE_SPEC_204 = new ResponseSpecBuilder().expectStatusCode(204).build(); LOAN_TRANSACTIONAL_HELPER = new LoanTransactionHelper(REQUEST_SPEC, RESPONSE_SPEC); LOAN_TRANSACTION_HELPER_403 = new LoanTransactionHelper(REQUEST_SPEC, RESPONSE_SPEC_403); - accountHelper = new AccountHelper(REQUEST_SPEC, RESPONSE_SPEC); - ClientHelper clientHelper = new ClientHelper(REQUEST_SPEC, RESPONSE_SPEC); - client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); - - final Account assetAccount = accountHelper.createAssetAccount(); - final Account incomeAccount = accountHelper.createIncomeAccount(); - final Account expenseAccount = accountHelper.createExpenseAccount(); - final Account overpaymentAccount = accountHelper.createLiabilityAccount(); + LOAN_TRANSACTIONAL_HELPER_204 = new LoanTransactionHelper(REQUEST_SPEC, RESPONSE_SPEC_204); + ACCOUNT_HELPER = new AccountHelper(REQUEST_SPEC, RESPONSE_SPEC); externalId = UUID.randomUUID().toString(); - loanProductId = createLoanProduct("500", "15", "4", true, "25", true, LoanScheduleType.PROGRESSIVE, - LoanScheduleProcessingType.HORIZONTAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); - - final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), loanProductId, BigDecimal.valueOf(500.0), 45, - 15, 3, BigDecimal.ZERO, "01 January 2023", "01 January 2023", externalId); - - loanId = loanResponse.getLoanId(); + createRequiredEntities(); Assertions.assertNotNull(loanProductId, "Loan Product ID should not be null after creation"); Assertions.assertNotNull(loanId, "Loan ID should not be null after creation"); Assertions.assertNotNull(externalId, "External Loan ID should not be null after creation"); } + /** + * Creates the client, loan product, and loan entities + **/ + private void createRequiredEntities() { + this.createClientEntity(); + this.createLoanProductEntity(); + this.createLoanEntity(); + } + @Test public void testCreateInterestPauseByLoanId_validRequest_shouldSucceed() { - PostLoansLoanIdTransactionsResponse response = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByLoanId("2023-01-01", "2023-02-05", + PostLoansLoanIdTransactionsResponse response = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByLoanId("2023-01-01", "2023-01-12", "yyyy-MM-dd", "en", loanId); Assertions.assertNotNull(response); @@ -118,7 +114,7 @@ public void testCreateInterestPauseByLoanId_validRequest_shouldSucceed() { @Test public void testCreateInterestPauseByLoanId_endDateBeforeStartDate_shouldFail() { try { - LOAN_TRANSACTION_HELPER_403.createInterestPauseByLoanId("2024-12-05", "2024-12-01", "yyyy-MM-dd", "en", loanId); + LOAN_TRANSACTION_HELPER_403.createInterestPauseByLoanId("2024-12-05", "2023-01-12", "yyyy-MM-dd", "en", loanId); } catch (Exception e) { String responseBody = e.getMessage(); Assertions.assertNotNull(responseBody, "Response body should not be null"); @@ -130,7 +126,7 @@ public void testCreateInterestPauseByLoanId_endDateBeforeStartDate_shouldFail() @Test public void testCreateInterestPauseByLoanId_startDateBeforeLoanStart_shouldFail() { try { - LOAN_TRANSACTION_HELPER_403.createInterestPauseByLoanId("2022-12-01", "2024-12-05", "yyyy-MM-dd", "en", loanId); + LOAN_TRANSACTION_HELPER_403.createInterestPauseByLoanId("2022-12-01", "2023-01-12", "yyyy-MM-dd", "en", loanId); } catch (Exception e) { String responseBody = e.getMessage(); Assertions.assertNotNull(responseBody, "Response body should not be null"); @@ -163,18 +159,18 @@ public void testRetrieveInterestPausesByLoanId_noPauses_shouldReturnEmpty() { @Test public void testRetrieveInterestPausesByLoanId_shouldReturnData() { - LOAN_TRANSACTIONAL_HELPER.createInterestPauseByLoanId("2023-01-01", "2023-02-05", "yyyy-MM-dd", "en", loanId); + LOAN_TRANSACTIONAL_HELPER.createInterestPauseByLoanId("2023-01-01", "2023-01-12", "yyyy-MM-dd", "en", loanId); String response = LOAN_TRANSACTIONAL_HELPER.retrieveInterestPauseByLoanId(loanId); Assertions.assertNotNull(response, "Response should not be null"); Assertions.assertTrue(response.contains("2023-01-01")); - Assertions.assertTrue(response.contains("2023-02-05")); + Assertions.assertTrue(response.contains("2023-01-12")); } @Test public void testCreateInterestPauseByExternalLoanId_validRequest_shouldSucceed() { - PostLoansLoanIdTransactionsResponse response = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByExternalId("2023-01-01", "2023-02-05", + PostLoansLoanIdTransactionsResponse response = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByExternalId("2023-01-01", "2023-01-12", "yyyy-MM-dd", "en", externalId); Assertions.assertNotNull(response); @@ -184,7 +180,7 @@ public void testCreateInterestPauseByExternalLoanId_validRequest_shouldSucceed() @Test public void testCreateInterestPauseByExternalLoanId_endDateBeforeStartDate_shouldFail() { try { - LOAN_TRANSACTION_HELPER_403.createInterestPauseByExternalId("2024-12-05", "2024-12-01", "yyyy-MM-dd", "en", externalId); + LOAN_TRANSACTION_HELPER_403.createInterestPauseByExternalId("2023-01-01", "2022-01-12", "yyyy-MM-dd", "en", externalId); } catch (Exception e) { String responseBody = e.getMessage(); Assertions.assertNotNull(responseBody, "Response body should not be null"); @@ -229,87 +225,207 @@ public void testRetrieveInterestPausesByExternalLoanId_noPauses_shouldReturnEmpt @Test public void testRetrieveInterestPausesByExternalLoanId_shouldReturnData() { - LOAN_TRANSACTIONAL_HELPER.createInterestPauseByExternalId("2023-01-01", "2023-02-05", "yyyy-MM-dd", "en", externalId); + LOAN_TRANSACTIONAL_HELPER.createInterestPauseByExternalId("2023-01-01", "2023-01-12", "yyyy-MM-dd", "en", externalId); String response = LOAN_TRANSACTIONAL_HELPER.retrieveInterestPauseByExternalId(externalId); Assertions.assertNotNull(response, "Response should not be null"); Assertions.assertTrue(response.contains("2023-01-01")); - Assertions.assertTrue(response.contains("2023-02-05")); + Assertions.assertTrue(response.contains("2023-01-12")); + } + + @Test + public void testUpdateInterestPauseByLoanId_validRequest_shouldSucceed() { + PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByLoanId("2023-01-01", + "2023-01-12", "yyyy-MM-dd", "en", loanId); + + Assertions.assertNotNull(createResponse); + Assertions.assertNotNull(createResponse.getResourceId()); + + Long variationId = createResponse.getResourceId(); + + PostLoansLoanIdTransactionsResponse updateResponse = LOAN_TRANSACTIONAL_HELPER.updateInterestPauseByLoanId(variationId, + "2023-01-01", "2023-01-12", "yyyy-MM-dd", "en", loanId); + + Assertions.assertNotNull(updateResponse); + Assertions.assertNotNull(updateResponse.getResourceId()); + Assertions.assertEquals(variationId, updateResponse.getResourceId()); + } + + @Test + public void testUpdateInterestPauseByLoanId_endDateBeforeStartDate_shouldFail() { + PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByLoanId("2023-01-01", + "2023-01-12", "yyyy-MM-dd", "en", loanId); + + Assertions.assertNotNull(createResponse); + Assertions.assertNotNull(createResponse.getResourceId()); + + Long variationId = createResponse.getResourceId(); + + try { + LOAN_TRANSACTION_HELPER_403.updateInterestPauseByLoanId(variationId, "2023-03-01", "2023-01-12", "yyyy-MM-dd", "en", loanId); + } catch (Exception e) { + String responseBody = e.getMessage(); + Assertions.assertNotNull(responseBody, "Response body should not be null"); + Assertions.assertTrue(responseBody.contains("interest.pause.end.date.before.start.date"), + "Response should contain the validation error message for end date before start date"); + } + } + + @Test + public void testUpdateInterestPauseByLoanId_startDateBeforeLoanStart_shouldFail() { + PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByLoanId("2023-01-01", + "2023-01-12", "yyyy-MM-dd", "en", loanId); + + Assertions.assertNotNull(createResponse); + Assertions.assertNotNull(createResponse.getResourceId()); + + Long variationId = createResponse.getResourceId(); + + try { + LOAN_TRANSACTION_HELPER_403.updateInterestPauseByLoanId(variationId, "2022-12-01", "2023-01-12", "yyyy-MM-dd", "en", loanId); + } catch (Exception e) { + String responseBody = e.getMessage(); + Assertions.assertNotNull(responseBody, "Response body should not be null"); + Assertions.assertTrue(responseBody.contains("interest.pause.start.date.before.loan.start.date"), + "Response should contain the validation error message for start date before loan start date"); + } } - private static Integer createLoanProduct(final String principal, final String repaymentAfterEvery, final String numberOfRepayments, - boolean downPaymentEnabled, String downPaymentPercentage, boolean autoPayForDownPayment, LoanScheduleType loanScheduleType, - LoanScheduleProcessingType loanScheduleProcessingType, final Account... accounts) { - AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); - final String loanProductJSON = new LoanProductTestBuilder().withMinPrincipal(principal).withPrincipal(principal) - .withRepaymentTypeAsDays().withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments) - .withEnableDownPayment(downPaymentEnabled, downPaymentPercentage, autoPayForDownPayment).withinterestRatePerPeriod("0") - .withInterestRateFrequencyTypeAsMonths() - .withRepaymentStrategy(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY) - .withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts) - .addAdvancedPaymentAllocation(defaultAllocation).withInterestCalculationPeriodTypeAsRepaymentPeriod(true) - .withInterestTypeAsDecliningBalance().withMultiDisburse().withDisallowExpectedDisbursements(true) - .withLoanScheduleType(loanScheduleType).withLoanScheduleProcessingType(loanScheduleProcessingType).withDaysInMonth("30") - .withDaysInYear("365").withMoratorium("0", "0").build(null); - return LOAN_TRANSACTIONAL_HELPER.getLoanProductId(loanProductJSON); + @Test + public void testDeleteInterestPauseByLoanId_validRequest_shouldSucceed() { + PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByLoanId("2023-01-01", + "2023-01-12", "yyyy-MM-dd", "en", loanId); + + Assertions.assertNotNull(createResponse, "Create response should not be null"); + Assertions.assertNotNull(createResponse.getResourceId(), "Resource ID should not be null"); + + Long variationId = createResponse.getResourceId(); + + try { + LOAN_TRANSACTIONAL_HELPER_204.deleteInterestPauseByLoanId(variationId, loanId); + } catch (Exception e) { + Assertions.fail("Delete operation failed: " + e.getMessage()); + } + + String response = LOAN_TRANSACTIONAL_HELPER.retrieveInterestPauseByLoanId(loanId); + Assertions.assertFalse(response.contains(String.valueOf(variationId)), "Response should not contain the deleted variation ID"); } - private static AdvancedPaymentData createDefaultPaymentAllocation() { - AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData(); - advancedPaymentData.setTransactionType("DEFAULT"); - advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT"); + @Test + public void testDeleteInterestPauseByLoanId_nonExistentVariation_shouldFail() { + try { + LOAN_TRANSACTION_HELPER_403.deleteInterestPauseByLoanId(99999L, loanId); + } catch (Exception e) { + String responseBody = e.getMessage(); + Assertions.assertNotNull(responseBody, "Response body should not be null"); + Assertions.assertTrue(responseBody.contains("error.msg.variation.not.found"), + "Response should contain the validation error message for variation not found"); + } + } - List paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY, - PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST, - PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL, - PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE, - PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST); + @Test + public void testDeleteInterestPauseByLoanId_invalidLoanId_shouldFail() { + PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByLoanId("2023-01-01", + "2023-01-12", "yyyy-MM-dd", "en", loanId); - advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders); - return advancedPaymentData; + Assertions.assertNotNull(createResponse); + Assertions.assertNotNull(createResponse.getResourceId()); + + Long variationId = createResponse.getResourceId(); + + try { + LOAN_TRANSACTION_HELPER_403.deleteInterestPauseByLoanId(variationId, nonExistLoanId); + } catch (Exception e) { + String responseBody = e.getMessage(); + Assertions.assertNotNull(responseBody, "Response body should not be null"); + Assertions.assertTrue(responseBody.contains("error.msg.variation.not.found"), + "Response should contain the validation error message for variation not found"); + } + } + + /** + * create a new client + **/ + private void createClientEntity() { + this.clientId = ClientHelper.createClient(REQUEST_SPEC, RESPONSE_SPEC); + + ClientHelper.verifyClientCreatedOnServer(REQUEST_SPEC, RESPONSE_SPEC, clientId); } - private static List getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) { - AtomicInteger integer = new AtomicInteger(1); - return Arrays.stream(paymentAllocationTypes).map(pat -> { - PaymentAllocationOrder paymentAllocationOrder = new PaymentAllocationOrder(); - paymentAllocationOrder.setPaymentAllocationRule(pat.name()); - paymentAllocationOrder.setOrder(integer.getAndIncrement()); - return paymentAllocationOrder; - }).collect(Collectors.toList()); + /** + * create a new loan product + **/ + private void createLoanProductEntity() { + LOG.info("---------------------------------CREATING LOAN PRODUCT------------------------------------------"); + + final String interestRecalculationCompoundingMethod = LoanProductTestBuilder.RECALCULATION_COMPOUNDING_METHOD_NONE; + final String rescheduleStrategyMethod = LoanProductTestBuilder.RECALCULATION_STRATEGY_ADJUST_LAST_UNPAID_PERIOD; + final String preCloseInterestCalculationStrategy = LoanProductTestBuilder.INTEREST_APPLICABLE_STRATEGY_ON_PRE_CLOSE_DATE; + + final Account assetAccount = ACCOUNT_HELPER.createAssetAccount(); + final Account incomeAccount = ACCOUNT_HELPER.createIncomeAccount(); + final Account expenseAccount = ACCOUNT_HELPER.createExpenseAccount(); + final Account overpaymentAccount = ACCOUNT_HELPER.createLiabilityAccount(); + + String futureInstallmentAllocationRule = "NEXT_INSTALLMENT"; + AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(futureInstallmentAllocationRule); + String loanProductJSON = new LoanProductTestBuilder().withPrincipal(loanPrincipalAmount).withNumberOfRepayments(numberOfRepayments) + .withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod(interestRatePerPeriod) + .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance() + .withAccountingRulePeriodicAccrual(new Account[] { assetAccount, incomeAccount, expenseAccount, overpaymentAccount }) + .withInterestCalculationPeriodTypeAsRepaymentPeriod(true).addAdvancedPaymentAllocation(defaultAllocation) + .withLoanScheduleType(LoanScheduleType.PROGRESSIVE).withLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL) + .withMultiDisburse().withDisallowExpectedDisbursements(true).withInterestRecalculationDetails( + interestRecalculationCompoundingMethod, rescheduleStrategyMethod, preCloseInterestCalculationStrategy) + .build(); + + loanProductId = LOAN_TRANSACTIONAL_HELPER.getLoanProductId(loanProductJSON); + LOG.info("Successfully created loan product (ID:{}) ", loanProductId); } - private static PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final BigDecimal principal, - final int loanTermFrequency, final int repaymentAfterEvery, final int numberOfRepayments, final BigDecimal interestRate, - final String expectedDisbursementDate, final String submittedOnDate, final String externalId) { - return applyForLoanApplication(clientId, loanProductId, principal, loanTermFrequency, repaymentAfterEvery, numberOfRepayments, - interestRate, expectedDisbursementDate, submittedOnDate, LoanScheduleProcessingType.HORIZONTAL, externalId); + /** + * submit a new loan application, approve and disburse the loan + **/ + private void createLoanEntity() { + LOG.info("---------------------------------NEW LOAN APPLICATION------------------------------------------"); + + String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(loanPrincipalAmount) + .withLoanTermFrequency(numberOfRepayments).withLoanTermFrequencyAsDays().withNumberOfRepayments(numberOfRepayments) + .withRepaymentEveryAfter("1").withRepaymentFrequencyTypeAsDays().withInterestRatePerPeriod(interestRatePerPeriod) + .withInterestTypeAsFlatBalance().withAmortizationTypeAsEqualPrincipalPayments() + .withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withExpectedDisbursementDate(dateString) + .withSubmittedOnDate(dateString).withLoanType("individual").withExternalId(externalId) + .withRepaymentStrategy("advanced-payment-allocation-strategy").build(clientId.toString(), loanProductId.toString(), null); + + loanId = LOAN_TRANSACTIONAL_HELPER.getLoanId(loanApplicationJSON); + + LOG.info("Sucessfully created loan (ID: {} )", loanId); + + approveLoanApplication(); + disburseLoan(); } - private static PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final BigDecimal principal, - final int loanTermFrequency, final int repaymentAfterEvery, final int numberOfRepayments, final BigDecimal interestRate, - final String expectedDisbursementDate, final String submittedOnDate, LoanScheduleProcessingType loanScheduleProcessingType, - final String externalId) { - LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); - return applyForLoanApplication(clientId, loanProductId, principal, loanTermFrequency, repaymentAfterEvery, numberOfRepayments, - interestRate, expectedDisbursementDate, submittedOnDate, - AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY, loanScheduleProcessingType.name(), - externalId); + /** + * approve the loan application + **/ + private void approveLoanApplication() { + + if (loanId != null) { + LOAN_TRANSACTIONAL_HELPER.approveLoan(dateString, loanId); + LOG.info("Successfully approved loan (ID: {} )", loanId); + } } - private static PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final BigDecimal principal, - final int loanTermFrequency, final int repaymentAfterEvery, final int numberOfRepayments, final BigDecimal interestRate, - final String expectedDisbursementDate, final String submittedOnDate, String transactionProcessorCode, - String loanScheduleProcessingType, final String externalId) { - LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); - return LOAN_TRANSACTIONAL_HELPER.applyLoan(new PostLoansRequest().clientId(clientId).productId(loanProductId.longValue()) - .expectedDisbursementDate(expectedDisbursementDate).dateFormat(DATETIME_PATTERN) - .transactionProcessingStrategyCode(transactionProcessorCode).locale("en").submittedOnDate(submittedOnDate) - .amortizationType(1).interestRatePerPeriod(interestRate).interestCalculationPeriodType(1).interestType(0) - .repaymentFrequencyType(0).repaymentEvery(repaymentAfterEvery).repaymentFrequencyType(0) - .numberOfRepayments(numberOfRepayments).loanTermFrequency(loanTermFrequency).loanTermFrequencyType(0).principal(principal) - .loanType("individual").loanScheduleProcessingType(loanScheduleProcessingType).externalId(externalId) - .maxOutstandingLoanBalance(BigDecimal.valueOf(35000))); + /** + * disburse the newly created loan + **/ + private void disburseLoan() { + + if (loanId != null) { + LOAN_TRANSACTIONAL_HELPER.disburseLoan(externalId, new PostLoansLoanIdRequest().actualDisbursementDate(dateString) + .transactionAmount(new BigDecimal(loanPrincipalAmount)).locale("en").dateFormat("dd MMMM yyyy")); + LOG.info("Successfully disbursed loan (ID: {} )", loanId); + } } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java index 40116b9aead..507d789fe85 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java @@ -66,6 +66,7 @@ public class LoanProductTestBuilder { public static final String RECALCULATION_STRATEGY_RESCHEDULE_NEXT_REPAYMENTS = "1"; public static final String RECALCULATION_STRATEGY_REDUCE_NUMBER_OF_INSTALLMENTS = "2"; public static final String RECALCULATION_STRATEGY_REDUCE_EMI_AMOUN = "3"; + public static final String RECALCULATION_STRATEGY_ADJUST_LAST_UNPAID_PERIOD = "4"; public static final String RECALCULATION_COMPOUNDING_METHOD_NONE = "0"; public static final String RECALCULATION_COMPOUNDING_METHOD_INTEREST = "1"; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 8785ec626af..52e222d65c0 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -609,7 +609,7 @@ public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final Long loanId, } public PostLoansLoanIdTransactionsResponse createInterestPauseByLoanId(final String startDate, final String endDate, - final String dateFormat, final String locale, final Long loanID) { + final String dateFormat, final String locale, final Integer loanID) { log.info("Creating interest pause for Loan {} from {} to {} with dateFormat {} and locale {}", loanID, startDate, endDate, dateFormat, locale); String body = getInterestPauseBodyAsJSON(startDate, endDate, dateFormat, locale); @@ -624,7 +624,20 @@ public PostLoansLoanIdTransactionsResponse createInterestPauseByExternalId(final return postLoanTransaction(createInterestPause(INTEREST_PAUSE_COMMAND, externalId), body); } - public String retrieveInterestPauseByLoanId(final Long loanID) { + public PostLoansLoanIdTransactionsResponse updateInterestPauseByLoanId(final Long termVariationId, final String startDate, + final String endDate, final String dateFormat, final String locale, final Integer loanID) { + log.info("Updating interest pause for Loan {} with Term Variation ID {}: startDate={} endDate={} dateFormat={} locale={}", loanID, + termVariationId, startDate, endDate, dateFormat, locale); + String body = getInterestPauseBodyAsJSON(startDate, endDate, dateFormat, locale); + return putLoanTransaction(updateInterestPause(termVariationId, loanID), body); + } + + public void deleteInterestPauseByLoanId(final Long termVariationId, final Integer loanID) { + log.info("Deleting interest pause for Loan ID {} with Term Variation ID {}", loanID, termVariationId); + deleteLoanTransaction(deleteInterestPause(termVariationId, loanID)); + } + + public String retrieveInterestPauseByLoanId(final Integer loanID) { log.info("Retrieving interest pauses for Loan ID {}", loanID); String url = retrieveInterestPause(loanID); return Utils.performServerGet(requestSpec, responseSpec, url); @@ -1450,7 +1463,7 @@ private String createLoanTransactionURL(final String command, final Integer loan return "/fineract-provider/api/v1/loans/" + loanID + "/transactions?command=" + command + "&" + Utils.TENANT_IDENTIFIER; } - private String createInterestPause(final String command, final Long loanID) { + private String createInterestPause(final String command, final Integer loanID) { return "/fineract-provider/api/v1/loans/" + loanID + "/interest-pauses?command=" + command + "&" + Utils.TENANT_IDENTIFIER; } @@ -1459,7 +1472,7 @@ private String createInterestPause(final String command, final String externalId + Utils.TENANT_IDENTIFIER; } - private String retrieveInterestPause(final Long loanID) { + private String retrieveInterestPause(final Integer loanID) { return "/fineract-provider/api/v1/loans/" + loanID + "/interest-pauses?" + Utils.TENANT_IDENTIFIER; } @@ -1467,6 +1480,14 @@ private String retrieveInterestPause(final String externalId) { return "/fineract-provider/api/v1/loans/external-id/" + externalId + "/interest-pauses?" + Utils.TENANT_IDENTIFIER; } + private String updateInterestPause(final Long termVariationId, final Integer loanID) { + return "/fineract-provider/api/v1/loans/" + loanID + "/interest-pauses/" + termVariationId + "?" + Utils.TENANT_IDENTIFIER; + } + + private String deleteInterestPause(final Long termVariationId, final Integer loanID) { + return "/fineract-provider/api/v1/loans/" + loanID + "/interest-pauses/" + termVariationId + "?" + Utils.TENANT_IDENTIFIER; + } + private String createInteroperationLoanTransactionURL(final String accountNo) { return "/fineract-provider/api/v1/interoperation/transactions/" + accountNo + "/loanrepayment"; } @@ -1513,6 +1534,15 @@ private PostLoansLoanIdTransactionsResponse postLoanTransaction(final String pos return GSON.fromJson(response, PostLoansLoanIdTransactionsResponse.class); } + private PostLoansLoanIdTransactionsResponse putLoanTransaction(final String putURLForLoanTransaction, final String jsonToBeSent) { + final String response = Utils.performServerPut(this.requestSpec, this.responseSpec, putURLForLoanTransaction, jsonToBeSent); + return GSON.fromJson(response, PostLoansLoanIdTransactionsResponse.class); + } + + private void deleteLoanTransaction(final String deleteURLForLoanTransaction) { + Utils.performServerDelete(this.requestSpec, this.responseSpec, deleteURLForLoanTransaction, null); + } + private Object performLoanTransaction(final String postURLForLoanTransaction, final String jsonToBeSent, ResponseSpecification responseValidationError) {