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
@@ -159,8 +159,7 @@ public class CommandSource extends AbstractPersistableCustom<Long> {
.transactionId(command.getTransactionId()) //
.creditBureauId(command.getCreditBureauId()) //
.organisationCreditBureauId(command.getOrganisationCreditBureauId()) //
.loanExternalId(command.getLoanExternalId()) //
.build(); //
.loanExternalId(command.getLoanExternalId()).build(); //
}
public String getPermissionCode() {
@@ -251,8 +251,16 @@ public class CommandWrapper {
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() {
@@ -3732,4 +3732,22 @@ public class CommandWrapperBuilder {
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;
}
}
@@ -249,8 +249,16 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
} 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());
}
@@ -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);
}
@@ -471,7 +471,7 @@ public class LoanAccountConfiguration {
@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);
}
}
@@ -180,4 +180,5 @@
<include file="parts/0159_trial_balance_summary_with_asset_owner_year_end_summary.xml" relativeToChangelogFile="true" />
<include file="parts/0160_add_acc_product_mapping_product_id_index.xml" relativeToChangelogFile="true" />
<include file="parts/0161_add_loan_external_id_to_commands.xml" relativeToChangelogFile="true" />
<include file="parts/0162_add_additional_audit_fields_to_term_variations.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet id="1" author="fineract">
<addColumn tableName="m_loan_term_variations">
<column name="created_by" type="BIGINT">
<constraints nullable="true"/>
</column>
</addColumn>
<addColumn tableName="m_loan_term_variations">
<column name="created_on_date" type="TIMESTAMP">
<constraints nullable="true"/>
</column>
</addColumn>
<addColumn tableName="m_loan_term_variations">
<column name="updated_by" type="BIGINT">
<constraints nullable="true"/>
</column>
</addColumn>
<addColumn tableName="m_loan_term_variations">
<column name="updated_on_date" type="TIMESTAMP">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -24,38 +24,30 @@ import io.restassured.http.ContentType;
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";
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";
@BeforeAll
public static void setupTests() {
@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 class LoanInterestPauseApiTest extends BaseLoanIntegrationTest {
@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 class LoanInterestPauseApiTest extends BaseLoanIntegrationTest {
@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 class LoanInterestPauseApiTest extends BaseLoanIntegrationTest {
@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 class LoanInterestPauseApiTest extends BaseLoanIntegrationTest {
@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 class LoanInterestPauseApiTest extends BaseLoanIntegrationTest {
@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"));
}
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 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());
}
private static AdvancedPaymentData createDefaultPaymentAllocation() {
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
advancedPaymentData.setTransactionType("DEFAULT");
advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT");
@Test
public void testUpdateInterestPauseByLoanId_endDateBeforeStartDate_shouldFail() {
PostLoansLoanIdTransactionsResponse createResponse = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByLoanId("2023-01-01",
"2023-01-12", "yyyy-MM-dd", "en", loanId);
List<PaymentAllocationOrder> 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);
Assertions.assertNotNull(createResponse);
Assertions.assertNotNull(createResponse.getResourceId());
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
return advancedPaymentData;
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");
}
}
private static List<PaymentAllocationOrder> 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());
@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 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);
@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 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);
@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");
}
}
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)));
@Test
public void testDeleteInterestPauseByLoanId_invalidLoanId_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.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);
}
/**
* 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);
}
/**
* 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();
}
/**
* approve the loan application
**/
private void approveLoanApplication() {
if (loanId != null) {
LOAN_TRANSACTIONAL_HELPER.approveLoan(dateString, loanId);
LOG.info("Successfully approved loan (ID: {} )", loanId);
}
}
/**
* 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);
}
}
}
@@ -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";
@@ -609,7 +609,7 @@ public class LoanTransactionHelper extends IntegrationTest {
}
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 class LoanTransactionHelper extends IntegrationTest {
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 @@ public class LoanTransactionHelper extends IntegrationTest {
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 @@ public class LoanTransactionHelper extends IntegrationTest {
+ 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 @@ public class LoanTransactionHelper extends IntegrationTest {
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 @@ public class LoanTransactionHelper extends IntegrationTest {
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) {