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 7e0ceca42..8b3ec4ad0 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 @@ -3759,6 +3759,15 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder deleteInterestPause(final String loanExternalId, final long variationId) { + this.actionName = "DELETE"; + this.entityName = "INTEREST_PAUSE"; + this.loanExternalId = new ExternalId(loanExternalId); + this.entityId = variationId; + this.href = "/v1/loans/external-id/" + loanExternalId + "/interest-pauses/" + variationId; + return this; + } + public CommandWrapperBuilder updateInterestPause(final long loanId, final long variationId) { this.actionName = "UPDATE"; this.entityName = "INTEREST_PAUSE"; @@ -3767,4 +3776,13 @@ public class CommandWrapperBuilder { this.href = "/v1/loans/" + loanId + "/interest-pauses/" + variationId; return this; } + + public CommandWrapperBuilder updateInterestPause(final String loanExternalId, final long variationId) { + this.actionName = "UPDATE"; + this.entityName = "INTEREST_PAUSE"; + this.loanExternalId = new ExternalId(loanExternalId); + this.entityId = variationId; + this.href = "/v1/loans/external-id/" + loanExternalId + "/interest-pauses/" + variationId; + return this; + } } 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 253727547..be6f13fd2 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 @@ -20,6 +20,9 @@ package org.apache.fineract.portfolio.interestpauses.api; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -64,7 +67,8 @@ public class LoanInterestPauseApiResource { @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Create a new interest pause period for a loan", description = "Allows users to define a period during which no interest will be accrued for a specific loan.") - @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Command successfully processed", content = @Content(schema = @Schema(implementation = CommandProcessingResult.class))) }) public CommandProcessingResult createInterestPause(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, @RequestBody(required = true) final InterestPauseRequestDto request) { @@ -80,7 +84,8 @@ public class LoanInterestPauseApiResource { @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Create a new interest pause for a loan using external ID", description = "Allows users to define a period during which no interest will be accrued for a specific loan using the external loan ID.") - @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Command successfully processed", content = @Content(schema = @Schema(implementation = CommandProcessingResult.class))) }) public CommandProcessingResult createInterestPauseByExternalId( @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId, @RequestBody(required = true) final InterestPauseRequestDto request) { @@ -97,7 +102,8 @@ public class LoanInterestPauseApiResource { @Path("/{loanId}/interest-pauses") @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Retrieve all interest pause periods for a loan", description = "Fetches a list of all active interest pause periods for a specific loan.") - @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "List of interest pause periods", content = @Content(array = @ArraySchema(schema = @Schema(implementation = InterestPauseResponseDto.class)))) }) public List retrieveInterestPauses( @PathParam("loanId") @Parameter(description = "loanId") final Long loanId) { @@ -110,7 +116,8 @@ public class LoanInterestPauseApiResource { @Path("/external-id/{loanExternalId}/interest-pauses") @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Retrieve all interest pause periods for a loan using external ID", description = "Fetches a list of all active interest pause periods for a specific loan using the external loan ID.") - @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "List of interest pause periods", content = @Content(array = @ArraySchema(schema = @Schema(implementation = InterestPauseResponseDto.class)))) }) public List retrieveInterestPausesByExternalId( @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId) { @@ -135,12 +142,30 @@ public class LoanInterestPauseApiResource { return Response.noContent().build(); } + @DELETE + @Path("/external-id/{loanExternalId}/interest-pauses/{variationId}") + @Operation(summary = "Delete an interest pause period by external id", description = "Deletes a specific interest pause period by its variation ID.") + @ApiResponses({ @ApiResponse(responseCode = "204", description = "No Content") }) + public Response deleteInterestPauseByExternalId( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId, + @PathParam("variationId") @Parameter(description = "variationId") final Long variationId) { + + this.context.authenticatedUser().validateHasReadPermission(MODIFY_RESOURCE_NAME_FOR_PERMISSIONS); + + final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteInterestPause(loanExternalId, 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") }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Command successfully processed", content = @Content(schema = @Schema(implementation = CommandProcessingResult.class))) }) 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) { @@ -152,4 +177,24 @@ public class LoanInterestPauseApiResource { return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } + + @PUT + @Path("/external-id/{loanExternalId}/interest-pauses/{variationId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Update an interest pause period by external id", description = "Updates a specific interest pause period by its variation ID.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Command successfully processed", content = @Content(schema = @Schema(implementation = CommandProcessingResult.class))) }) + public CommandProcessingResult updateInterestPauseByExternalId( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId, + @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(loanExternalId, variationId) + .withJson(request.toJson()).build(); + + return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } } 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 index 8068c8c5a..f620952c2 100644 --- 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 @@ -22,6 +22,8 @@ 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.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.portfolio.interestpauses.service.InterestPauseWritePlatformService; import org.springframework.stereotype.Component; @@ -34,8 +36,16 @@ public class DeleteInterestPauseCommandHandler implements NewCommandSourceHandle @Override public CommandProcessingResult processCommand(final JsonCommand command) { final Long loanId = command.getLoanId(); + final ExternalId loanExternalId = command.getLoanExternalId(); final Long termVariationId = command.getResourceId(); - return interestPauseService.deleteInterestPause(loanId, termVariationId); + if (loanId != null) { + return interestPauseService.deleteInterestPause(loanId, termVariationId); + } else if (loanExternalId != null) { + return interestPauseService.deleteInterestPause(loanExternalId, termVariationId); + } else { + throw new PlatformApiDataValidationException("validation.msg.missing.loan.id.or.external.id", + "Either loanId or loanExternalId must be provided.", "loanId"); + } } } 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 index 04a9b2f03..f4bda33a6 100644 --- 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 @@ -22,6 +22,8 @@ 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.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.portfolio.interestpauses.service.InterestPauseWritePlatformService; import org.springframework.stereotype.Component; @@ -34,12 +36,20 @@ public class UpdateInterestPauseCommandHandler implements NewCommandSourceHandle @Override public CommandProcessingResult processCommand(final JsonCommand command) { final Long loanId = command.getLoanId(); + final ExternalId loanExternalId = command.getLoanExternalId(); 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); + if (loanId != null) { + return interestPauseService.updateInterestPause(loanId, termVariationId, startDate, endDate, dateFormat, locale); + } else if (loanExternalId != null) { + return interestPauseService.updateInterestPause(loanExternalId, termVariationId, startDate, endDate, dateFormat, locale); + } else { + throw new PlatformApiDataValidationException("validation.msg.missing.loan.id.or.external.id", + "Either loanId or loanExternalId must be provided.", "loanId"); + } } } 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 491a45171..fdf3122ee 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 @@ -69,6 +69,17 @@ public interface InterestPauseWritePlatformService { */ CommandProcessingResult deleteInterestPause(Long loanId, Long variationId); + /** + * Delete an existing interest pause period for a loan. + * + * @param loanExternalId + * the external 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(ExternalId loanExternalId, Long variationId); + /** * Update an existing interest pause period for a loan identified by its internal ID. * @@ -88,4 +99,24 @@ public interface InterestPauseWritePlatformService { */ CommandProcessingResult updateInterestPause(Long loanId, Long variationId, String startDateString, String endDateString, String dateFormat, String locale); + + /** + * Update an existing interest pause period for a loan identified by its internal ID. + * + * @param loanExternalId + * the external 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(ExternalId loanExternalId, 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 8076b4062..334f075a8 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 @@ -31,7 +31,6 @@ 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; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; @@ -40,7 +39,6 @@ import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; 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.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.LoanTermVariations; @@ -53,7 +51,6 @@ 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, @@ -61,8 +58,8 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite final LocalDate startDate = parseDate(startDateString, dateFormat, locale); final LocalDate endDate = parseDate(endDateString, dateFormat, locale); - return processInterestPause(() -> loanRepositoryWrapper.findOneWithNotFoundDetection(loanExternalId), startDate, endDate, - dateFormat, locale); + return processInterestPause(loanRepositoryWrapper.findOneWithNotFoundDetection(loanExternalId), startDate, endDate, dateFormat, + locale); } @Override @@ -71,14 +68,38 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite final LocalDate startDate = parseDate(startDateString, dateFormat, locale); final LocalDate endDate = parseDate(endDateString, dateFormat, locale); - return processInterestPause(() -> loanRepositoryWrapper.findOneWithNotFoundDetection(loanId), startDate, endDate, dateFormat, - locale); + return processInterestPause(loanRepositoryWrapper.findOneWithNotFoundDetection(loanId), startDate, endDate, dateFormat, locale); + } + + @Override + public CommandProcessingResult deleteInterestPause(ExternalId loanExternalId, Long variationId) { + return processDeleteInterestPause(loanRepositoryWrapper.findOneWithNotFoundDetection(loanExternalId), variationId); } @Override public CommandProcessingResult deleteInterestPause(Long loanId, Long variationId) { + return processDeleteInterestPause(loanRepositoryWrapper.findOneWithNotFoundDetection(loanId), variationId); + } + + @Override + public CommandProcessingResult updateInterestPause(ExternalId loanExternalId, Long variationId, String startDateString, + String endDateString, String dateFormat, String locale) { + return processUpdateInterestPause(loanRepositoryWrapper.findOneWithNotFoundDetection(loanExternalId), variationId, startDateString, + endDateString, dateFormat, locale); + } + + @Override + public CommandProcessingResult updateInterestPause(Long loanId, Long variationId, String startDateString, String endDateString, + String dateFormat, String locale) { + return processUpdateInterestPause(loanRepositoryWrapper.findOneWithNotFoundDetection(loanId), variationId, startDateString, + endDateString, dateFormat, locale); + } + + private CommandProcessingResult processDeleteInterestPause(Loan loan, Long variationId) { + validateActiveLoan(loan); + LoanTermVariations variation = loanTermVariationsRepository - .findByIdAndLoanIdAndTermType(variationId, loanId, INTEREST_PAUSE.getValue()) + .findByIdAndLoanIdAndTermType(variationId, loan.getId(), INTEREST_PAUSE.getValue()) .orElseThrow(() -> new GeneralPlatformDomainRuleException("error.msg.variation.not.found", "Variation not found for the given loan ID")); @@ -87,18 +108,16 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite return new CommandProcessingResultBuilder().withEntityId(variationId).build(); } - @Override - public CommandProcessingResult updateInterestPause(Long loanId, Long variationId, String startDateString, String endDateString, + private CommandProcessingResult processUpdateInterestPause(Loan loan, 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); + validateActiveLoan(loan); validateInterestPauseDates(loan, startDate, endDate, dateFormat, locale); LoanTermVariations variation = loanTermVariationsRepository - .findByIdAndLoanIdAndTermType(variationId, loanId, INTEREST_PAUSE.getValue()) + .findByIdAndLoanIdAndTermType(variationId, loan.getId(), INTEREST_PAUSE.getValue()) .orElseThrow(() -> new GeneralPlatformDomainRuleException("error.msg.variation.not.found", "Variation not found for the given loan ID")); @@ -113,10 +132,9 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite .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(); - + private CommandProcessingResult processInterestPause(Loan loan, LocalDate startDate, LocalDate endDate, String dateFormat, + String locale) { + validateActiveLoan(loan); validateInterestPauseDates(loan, startDate, endDate, dateFormat, locale); LoanTermVariations variation = new LoanTermVariations(INTEREST_PAUSE.getValue(), startDate, null, endDate, false, loan); @@ -154,11 +172,6 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite 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."); @@ -168,6 +181,22 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite throw new GeneralPlatformDomainRuleException("loan.must.have.recalculate.interest.enabled", "Interest pause is only supported for loans with recalculate interest enabled."); } + + for (LoanTermVariations existingVariation : loan.getLoanTermVariations()) { + if (Objects.equals(existingVariation.getTermType().getValue(), INTEREST_PAUSE.getValue())) { + if (!(endDate.isBefore(existingVariation.getTermApplicableFrom()) || startDate.isAfter(existingVariation.getDateValue()))) { + throw new GeneralPlatformDomainRuleException("interest.pause.overlapping", + "Overlapping interest pauses are not allowed."); + } + } + } + } + + private void validateActiveLoan(Loan loan) { + if (!Objects.equals(loan.getLoanStatus(), ACTIVE.getValue())) { + throw new GeneralPlatformDomainRuleException("loan.must.be.active", + "Operations on interest pauses are restricted to active loans."); + } } private void validateVariations(Loan loan, LoanTermVariations variation) { @@ -178,9 +207,13 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite 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."); + if (!existingVariation.equals(variation) + && Objects.equals(existingVariation.getTermType().getValue(), INTEREST_PAUSE.getValue())) { + if (!(variation.getDateValue().isBefore(existingVariation.getTermApplicableFrom()) + || variation.getTermApplicableFrom().isAfter(existingVariation.getDateValue()))) { + throw new GeneralPlatformDomainRuleException("interest.pause.overlapping", + "Overlapping interest pauses are not allowed."); + } } } } 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 a1a1fddbb..b7b9a955d 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 class LoanAccountConfiguration { @Bean @ConditionalOnMissingBean(InterestPauseWritePlatformService.class) public InterestPauseWritePlatformService interestPauseWritePlatformService(LoanTermVariationsRepository loanTermVariationsRepository, - LoanRepositoryWrapper loanRepositoryWrapper, PlatformSecurityContext context) { - return new InterestPauseWritePlatformServiceImpl(loanTermVariationsRepository, loanRepositoryWrapper, context); + LoanRepositoryWrapper loanRepositoryWrapper) { + return new InterestPauseWritePlatformServiceImpl(loanTermVariationsRepository, loanRepositoryWrapper); } } 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 d4f5d0dc0..6af4626b6 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 @@ -55,10 +55,12 @@ 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_404; 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 LoanTransactionHelper LOAN_TRANSACTION_HELPER_404; private static AccountHelper ACCOUNT_HELPER; private static final Integer nonExistLoanId = 99999; private static String externalId; @@ -78,9 +80,11 @@ public class LoanInterestPauseApiTest extends BaseLoanIntegrationTest { REQUEST_SPEC.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); RESPONSE_SPEC = new ResponseSpecBuilder().expectStatusCode(200).build(); RESPONSE_SPEC_403 = new ResponseSpecBuilder().expectStatusCode(403).build(); + RESPONSE_SPEC_404 = new ResponseSpecBuilder().expectStatusCode(404).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); + LOAN_TRANSACTION_HELPER_404 = new LoanTransactionHelper(REQUEST_SPEC, RESPONSE_SPEC_404); LOAN_TRANSACTIONAL_HELPER_204 = new LoanTransactionHelper(REQUEST_SPEC, RESPONSE_SPEC_204); ACCOUNT_HELPER = new AccountHelper(REQUEST_SPEC, RESPONSE_SPEC); @@ -235,7 +239,7 @@ public class LoanInterestPauseApiTest extends BaseLoanIntegrationTest { } @Test - public void testUpdateInterestPauseByLoanId_validRequest_shouldSucceed() { + public void testUpdateInterestPauseByLoanId_overlapping_shouldFail() { PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByLoanId("2023-01-01", "2023-01-12", "yyyy-MM-dd", "en", loanId); @@ -244,8 +248,68 @@ public class LoanInterestPauseApiTest extends BaseLoanIntegrationTest { Long variationId = createResponse.getResourceId(); + try { + LOAN_TRANSACTION_HELPER_403.updateInterestPauseByLoanId(variationId, "2023-01-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.overlapping"), + "Response should contain the validation error message for end date after loan maturity date"); + } + } + + @Test + public void testUpdateInterestPauseByLoanId_overlapping_shouldFail2() { + 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-01-02", "2023-01-13", "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.overlapping"), + "Response should contain the validation error message for end date after loan maturity date"); + } + } + + @Test + public void testUpdateInterestPauseByLoanId_overlapping_shouldFail3() { + PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByLoanId("2023-01-02", + "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-01-01", "2023-01-11", "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.overlapping"), + "Response should contain the validation error message for end date after loan maturity date"); + } + } + + @Test + public void testUpdateInterestPauseByLoanId_validRequest_shouldSucceed() { + PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByLoanId("2023-01-01", + "2023-01-02", "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); + "2023-01-03", "2023-01-04", "yyyy-MM-dd", "en", loanId); Assertions.assertNotNull(updateResponse); Assertions.assertNotNull(updateResponse.getResourceId()); @@ -335,7 +399,159 @@ public class LoanInterestPauseApiTest extends BaseLoanIntegrationTest { Long variationId = createResponse.getResourceId(); try { - LOAN_TRANSACTION_HELPER_403.deleteInterestPauseByLoanId(variationId, nonExistLoanId); + LOAN_TRANSACTION_HELPER_404.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.loan.id.invalid"), + "Response should contain the validation error message for variation not found"); + } + } + + @Test + public void testUpdateInterestPauseByExternalId_validRequest_shouldSucceed() { + PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByExternalId("2023-01-01", + "2023-01-02", "yyyy-MM-dd", "en", externalId); + + Assertions.assertNotNull(createResponse); + Assertions.assertNotNull(createResponse.getResourceId()); + + Long variationId = createResponse.getResourceId(); + + PostLoansLoanIdTransactionsResponse updateResponse = LOAN_TRANSACTIONAL_HELPER.updateInterestPauseByExternalId(variationId, + "2023-01-03", "2023-01-04", "yyyy-MM-dd", "en", externalId); + + Assertions.assertNotNull(updateResponse); + Assertions.assertNotNull(updateResponse.getResourceId()); + Assertions.assertEquals(variationId, updateResponse.getResourceId()); + } + + @Test + public void testUpdateInterestPauseByExternalId_overlapping_shouldFail() { + PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByExternalId("2023-01-01", + "2023-01-12", "yyyy-MM-dd", "en", externalId); + + Assertions.assertNotNull(createResponse); + Assertions.assertNotNull(createResponse.getResourceId()); + + Long variationId = createResponse.getResourceId(); + try { + LOAN_TRANSACTION_HELPER_403.updateInterestPauseByExternalId(variationId, "2023-01-01", "2023-01-12", "yyyy-MM-dd", "en", + externalId); + } catch (Exception e) { + String responseBody = e.getMessage(); + Assertions.assertNotNull(responseBody, "Response body should not be null"); + Assertions.assertTrue(responseBody.contains("interest.pause.overlapping"), + "Response should contain the validation error message for end date after loan maturity date"); + } + } + + @Test + public void testUpdateInterestPauseByExternalId_overlapping_shouldFail2() { + PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByExternalId("2023-01-01", + "2023-01-12", "yyyy-MM-dd", "en", externalId); + + Assertions.assertNotNull(createResponse); + Assertions.assertNotNull(createResponse.getResourceId()); + + Long variationId = createResponse.getResourceId(); + try { + LOAN_TRANSACTION_HELPER_403.updateInterestPauseByExternalId(variationId, "2023-01-02", "2023-01-13", "yyyy-MM-dd", "en", + externalId); + } catch (Exception e) { + String responseBody = e.getMessage(); + Assertions.assertNotNull(responseBody, "Response body should not be null"); + Assertions.assertTrue(responseBody.contains("interest.pause.overlapping"), + "Response should contain the validation error message for end date after loan maturity date"); + } + } + + @Test + public void testUpdateInterestPauseByExternalId_overlapping_shouldFail3() { + PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByExternalId("2023-01-02", + "2023-01-12", "yyyy-MM-dd", "en", externalId); + + Assertions.assertNotNull(createResponse); + Assertions.assertNotNull(createResponse.getResourceId()); + + Long variationId = createResponse.getResourceId(); + try { + LOAN_TRANSACTION_HELPER_403.updateInterestPauseByExternalId(variationId, "2023-01-01", "2023-01-11", "yyyy-MM-dd", "en", + externalId); + } catch (Exception e) { + String responseBody = e.getMessage(); + Assertions.assertNotNull(responseBody, "Response body should not be null"); + Assertions.assertTrue(responseBody.contains("interest.pause.overlapping"), + "Response should contain the validation error message for end date after loan maturity date"); + } + } + + @Test + public void testUpdateInterestPauseByExternalId_endDateBeforeStartDate_shouldFail() { + PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByExternalId("2023-01-01", + "2023-01-12", "yyyy-MM-dd", "en", externalId); + + Assertions.assertNotNull(createResponse); + Assertions.assertNotNull(createResponse.getResourceId()); + + Long variationId = createResponse.getResourceId(); + + try { + LOAN_TRANSACTION_HELPER_403.updateInterestPauseByExternalId(variationId, "2023-03-01", "2023-01-12", "yyyy-MM-dd", "en", + externalId); + } 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 testUpdateInterestPauseByExternalId_startDateBeforeLoanStart_shouldFail() { + PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByExternalId("2023-01-01", + "2023-01-12", "yyyy-MM-dd", "en", externalId); + + Assertions.assertNotNull(createResponse); + Assertions.assertNotNull(createResponse.getResourceId()); + + Long variationId = createResponse.getResourceId(); + + try { + LOAN_TRANSACTION_HELPER_403.updateInterestPauseByExternalId(variationId, "2022-12-01", "2023-01-12", "yyyy-MM-dd", "en", + externalId); + } 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"); + } + } + + @Test + public void testDeleteInterestPauseByExternalId_validRequest_shouldSucceed() { + PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByExternalId("2023-01-01", + "2023-01-12", "yyyy-MM-dd", "en", externalId); + + 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.deleteInterestPauseByExternalId(variationId, externalId); + } catch (Exception e) { + Assertions.fail("Delete operation failed: " + e.getMessage()); + } + + String response = LOAN_TRANSACTIONAL_HELPER.retrieveInterestPauseByExternalId(externalId); + Assertions.assertFalse(response.contains(String.valueOf(variationId)), "Response should not contain the deleted variation ID"); + } + + @Test + public void testDeleteInterestPauseByExternalId_nonExistentVariation_shouldFail() { + try { + LOAN_TRANSACTION_HELPER_403.deleteInterestPauseByExternalId(99999L, externalId); } catch (Exception e) { String responseBody = e.getMessage(); Assertions.assertNotNull(responseBody, "Response body should not be null"); @@ -344,6 +560,26 @@ public class LoanInterestPauseApiTest extends BaseLoanIntegrationTest { } } + @Test + public void testDeleteInterestPauseByExternalId_invalidExternalId_shouldFail() { + PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByExternalId("2023-01-01", + "2023-01-12", "yyyy-MM-dd", "en", externalId); + + Assertions.assertNotNull(createResponse); + Assertions.assertNotNull(createResponse.getResourceId()); + + Long variationId = createResponse.getResourceId(); + + try { + LOAN_TRANSACTION_HELPER_404.deleteInterestPauseByExternalId(variationId, nonExistExternalId); + } catch (Exception e) { + String responseBody = e.getMessage(); + Assertions.assertNotNull(responseBody, "Response body should not be null"); + Assertions.assertTrue(responseBody.contains("error.msg.loan.external.id.invalid"), + "Response should contain the validation error message for variation not found"); + } + } + /** * create a new client **/ 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 52e222d65..ed8aadb95 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 @@ -632,11 +632,24 @@ public class LoanTransactionHelper extends IntegrationTest { return putLoanTransaction(updateInterestPause(termVariationId, loanID), body); } + public PostLoansLoanIdTransactionsResponse updateInterestPauseByExternalId(final Long termVariationId, final String startDate, + final String endDate, final String dateFormat, final String locale, final String externalID) { + log.info("Updating interest pause for Loan {} with Term Variation ID {}: startDate={} endDate={} dateFormat={} locale={}", + externalID, termVariationId, startDate, endDate, dateFormat, locale); + String body = getInterestPauseBodyAsJSON(startDate, endDate, dateFormat, locale); + return putLoanTransaction(updateInterestPause(termVariationId, externalID), 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 void deleteInterestPauseByExternalId(final Long termVariationId, final String externalID) { + log.info("Deleting interest pause for Loan ID {} with Term Variation ID {}", externalID, termVariationId); + deleteLoanTransaction(deleteInterestPause(termVariationId, externalID)); + } + public String retrieveInterestPauseByLoanId(final Integer loanID) { log.info("Retrieving interest pauses for Loan ID {}", loanID); String url = retrieveInterestPause(loanID); @@ -1484,10 +1497,20 @@ public class LoanTransactionHelper extends IntegrationTest { return "/fineract-provider/api/v1/loans/" + loanID + "/interest-pauses/" + termVariationId + "?" + Utils.TENANT_IDENTIFIER; } + private String updateInterestPause(final Long termVariationId, final String externalID) { + return "/fineract-provider/api/v1/loans/external-id/" + externalID + "/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 deleteInterestPause(final Long termVariationId, final String externalID) { + return "/fineract-provider/api/v1/loans/external-id/" + externalID + "/interest-pauses/" + termVariationId + "?" + + Utils.TENANT_IDENTIFIER; + } + private String createInteroperationLoanTransactionURL(final String accountNo) { return "/fineract-provider/api/v1/interoperation/transactions/" + accountNo + "/loanrepayment"; }