FINERACT-2152: API update and delete interest pause

This commit is contained in:
Andrii Kulminskyi
2025-01-08 16:18:44 +01:00
committed by Adam Saghy
parent d2afd1aac7
commit 8663c054a0
18 changed files with 674 additions and 130 deletions
@@ -25,12 +25,15 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
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 @@ import org.springframework.stereotype.Component;
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 class LoanInterestPauseApiResource {
@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 class LoanInterestPauseApiResource {
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);
}
}
@@ -27,16 +27,14 @@ import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidati
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 class InterestPauseCommandHandler implements NewCommandSourceHandler {
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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -23,6 +23,21 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId;
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 @@ public interface InterestPauseWritePlatformService {
* 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);
}
@@ -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.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.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 class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite
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<Loan> 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 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite
.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<LoanTermVariations> 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) {
@@ -27,6 +27,7 @@ import jakarta.persistence.OneToOne;
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<Long> {
@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 class LoanTermVariations extends AbstractPersistableCustom<Long> {
return this.dateValue;
}
public void setDateValue(LocalDate dateValue) {
this.dateValue = dateValue;
}
public void setTermApplicableFrom(LocalDate termApplicableFrom) {
this.termApplicableFrom = termApplicableFrom;
}
@@ -167,4 +184,35 @@ public class LoanTermVariations extends AbstractPersistableCustom<Long> {
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;
}
}
@@ -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<LoanTermVariationsData> 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<LoanTermVariations> findByIdAndLoanIdAndTermType(@Param("variationId") long variationId, @Param("loanId") long loanId,
@Param("termType") int termType);
}