FINERACT-2152: API Create and retrieve interest pause

This commit is contained in:
Andrii Kulminskyi
2024-12-19 22:24:02 +01:00
committed by Adam Saghy
parent e94e6c421a
commit c2ec015e64
39 changed files with 1112 additions and 43 deletions
@@ -133,6 +133,9 @@ public class CommandSource extends AbstractPersistableCustom<Long> {
@Column(name = "result_status_code")
private Integer resultStatusCode;
@Column(name = "loan_external_id", length = 100)
private ExternalId loanExternalId;
public static CommandSource fullEntryFrom(final CommandWrapper wrapper, final JsonCommand command, final AppUser maker,
String idempotencyKey, Integer status) {
@@ -156,6 +159,7 @@ public class CommandSource extends AbstractPersistableCustom<Long> {
.transactionId(command.getTransactionId()) //
.creditBureauId(command.getCreditBureauId()) //
.organisationCreditBureauId(command.getOrganisationCreditBureauId()) //
.loanExternalId(command.getLoanExternalId()) //
.build(); //
}
@@ -195,5 +199,6 @@ public class CommandSource extends AbstractPersistableCustom<Long> {
this.resourceExternalId = result.getResourceExternalId();
this.subResourceId = result.getSubResourceId();
this.subResourceExternalId = result.getSubResourceExternalId();
this.loanExternalId = result.getLoanExternalId();
}
}
@@ -19,6 +19,7 @@
package org.apache.fineract.commands.domain;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.useradministration.api.PasswordPreferencesApiConstants;
@Getter
@@ -43,6 +44,7 @@ public class CommandWrapper {
private final Long creditBureauId;
private final Long organisationCreditBureauId;
private final String jobName;
private final ExternalId loanExternalId;
private final String idempotencyKey;
@@ -61,9 +63,11 @@ public class CommandWrapper {
public static CommandWrapper fromExistingCommand(final Long commandId, final String actionName, final String entityName,
final Long resourceId, final Long subresourceId, final String resourceGetUrl, final Long productId, final Long officeId,
final Long groupId, final Long clientId, final Long loanId, final Long savingsId, final String transactionId,
final Long creditBureauId, final Long organisationCreditBureauId, final String idempotencyKey) {
final Long creditBureauId, final Long organisationCreditBureauId, final String idempotencyKey,
final ExternalId loanExternalId) {
return new CommandWrapper(commandId, actionName, entityName, resourceId, subresourceId, resourceGetUrl, productId, officeId,
groupId, clientId, loanId, savingsId, transactionId, creditBureauId, organisationCreditBureauId, idempotencyKey);
groupId, clientId, loanId, savingsId, transactionId, creditBureauId, organisationCreditBureauId, idempotencyKey,
loanExternalId);
}
private CommandWrapper(final Long commandId, final String actionName, final String entityName, final Long resourceId,
@@ -87,12 +91,13 @@ public class CommandWrapper {
this.organisationCreditBureauId = null;
this.jobName = null;
this.idempotencyKey = null;
this.loanExternalId = null;
}
public CommandWrapper(final Long officeId, final Long groupId, final Long clientId, final Long loanId, final Long savingsId,
final String actionName, final String entityName, final Long entityId, final Long subentityId, final String href,
final String json, final String transactionId, final Long productId, final Long templateId, final Long creditBureauId,
final Long organisationCreditBureauId, final String jobName, final String idempotencyKey) {
final Long organisationCreditBureauId, final String jobName, final String idempotencyKey, final ExternalId loanExternalId) {
this.commandId = null;
this.officeId = officeId;
@@ -114,12 +119,13 @@ public class CommandWrapper {
this.organisationCreditBureauId = organisationCreditBureauId;
this.jobName = jobName;
this.idempotencyKey = idempotencyKey;
this.loanExternalId = loanExternalId;
}
private CommandWrapper(final Long commandId, final String actionName, final String entityName, final Long resourceId,
final Long subresourceId, final String resourceGetUrl, final Long productId, final Long officeId, final Long groupId,
final Long clientId, final Long loanId, final Long savingsId, final String transactionId, final Long creditBureauId,
final Long organisationCreditBureauId, final String idempotencyKey) {
final Long organisationCreditBureauId, final String idempotencyKey, final ExternalId loanExternalId) {
this.commandId = commandId;
this.officeId = officeId;
@@ -140,6 +146,7 @@ public class CommandWrapper {
this.organisationCreditBureauId = organisationCreditBureauId;
this.jobName = null;
this.idempotencyKey = idempotencyKey;
this.loanExternalId = loanExternalId;
}
public boolean isCreate() {
@@ -240,6 +247,14 @@ public class CommandWrapper {
return this.entityName.equalsIgnoreCase(PasswordPreferencesApiConstants.ENTITY_NAME);
}
public boolean isInterestPauseResource() {
return this.entityName.equalsIgnoreCase("INTEREST_PAUSE");
}
public boolean isInterestPauseExternalIdResource() {
return this.entityName.equalsIgnoreCase("INTEREST_PAUSE") && this.href.contains("/external-id/");
}
public Long commandId() {
return this.commandId;
}
@@ -21,6 +21,7 @@ package org.apache.fineract.commands.service;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.infrastructure.accountnumberformat.service.AccountNumberFormatConstants;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.portfolio.client.api.ClientApiConstants;
import org.apache.fineract.portfolio.paymenttype.api.PaymentTypeApiResourceConstants;
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
@@ -47,18 +48,19 @@ public class CommandWrapperBuilder {
private Long organisationCreditBureauId;
private String jobName;
private String idempotencyKey;
private ExternalId loanExternalId;
@SuppressFBWarnings(value = "UWF_UNWRITTEN_FIELD", justification = "TODO: fix this!")
public CommandWrapper build() {
return new CommandWrapper(this.officeId, this.groupId, this.clientId, this.loanId, this.savingsId, this.actionName, this.entityName,
this.entityId, this.subentityId, this.href, this.json, this.transactionId, this.productId, this.templateId,
this.creditBureauId, this.organisationCreditBureauId, this.jobName, this.idempotencyKey);
this.creditBureauId, this.organisationCreditBureauId, this.jobName, this.idempotencyKey, this.loanExternalId);
}
public CommandWrapper build(String idempotencyKey) {
return new CommandWrapper(this.officeId, this.groupId, this.clientId, this.loanId, this.savingsId, this.actionName, this.entityName,
this.entityId, this.subentityId, this.href, this.json, this.transactionId, this.productId, this.templateId,
this.creditBureauId, this.organisationCreditBureauId, this.jobName, idempotencyKey);
this.creditBureauId, this.organisationCreditBureauId, this.jobName, idempotencyKey, this.loanExternalId);
}
public CommandWrapperBuilder updateCreditBureau() {
@@ -3714,4 +3716,20 @@ public class CommandWrapperBuilder {
this.href = "/loans/" + loanId + "/delinquency-action";
return this;
}
public CommandWrapperBuilder createInterestPause(final long loanId) {
this.actionName = "CREATE";
this.entityName = "INTEREST_PAUSE";
this.loanId = loanId;
this.href = "/v1/loans/" + loanId + "/interest-pauses";
return this;
}
public CommandWrapperBuilder createInterestPauseByExternalId(final String loanExternalId) {
this.actionName = "CREATE";
this.entityName = "INTEREST_PAUSE";
this.loanExternalId = new ExternalId(loanExternalId);
this.href = "/v1/loans/external-id/" + loanExternalId + "/interest-pauses";
return this;
}
}
@@ -73,7 +73,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
JsonCommand command = JsonCommand.from(json, parsedCommand, this.fromApiJsonHelper, wrapper.getEntityName(), wrapper.getEntityId(),
wrapper.getSubentityId(), wrapper.getGroupId(), wrapper.getClientId(), wrapper.getLoanId(), wrapper.getSavingsId(),
wrapper.getTransactionId(), wrapper.getHref(), wrapper.getProductId(), wrapper.getCreditBureauId(),
wrapper.getOrganisationCreditBureauId(), wrapper.getJobName());
wrapper.getOrganisationCreditBureauId(), wrapper.getJobName(), wrapper.getLoanExternalId());
return this.processAndLogCommandService.executeCommand(wrapper, command, isApprovedByChecker);
}
@@ -88,14 +88,16 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
commandSourceInput.getResourceGetUrl(), commandSourceInput.getProductId(), commandSourceInput.getOfficeId(),
commandSourceInput.getGroupId(), commandSourceInput.getClientId(), commandSourceInput.getLoanId(),
commandSourceInput.getSavingsId(), commandSourceInput.getTransactionId(), commandSourceInput.getCreditBureauId(),
commandSourceInput.getOrganisationCreditBureauId(), commandSourceInput.getIdempotencyKey());
commandSourceInput.getOrganisationCreditBureauId(), commandSourceInput.getIdempotencyKey(),
commandSourceInput.getLoanExternalId());
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(commandSourceInput.getCommandAsJson());
final JsonCommand command = JsonCommand.fromExistingCommand(makerCheckerId, commandSourceInput.getCommandAsJson(), parsedCommand,
this.fromApiJsonHelper, commandSourceInput.getEntityName(), commandSourceInput.getResourceId(),
commandSourceInput.getSubResourceId(), commandSourceInput.getGroupId(), commandSourceInput.getClientId(),
commandSourceInput.getLoanId(), commandSourceInput.getSavingsId(), commandSourceInput.getTransactionId(),
commandSourceInput.getResourceGetUrl(), commandSourceInput.getProductId(), commandSourceInput.getCreditBureauId(),
commandSourceInput.getOrganisationCreditBureauId(), commandSourceInput.getJobName());
commandSourceInput.getOrganisationCreditBureauId(), commandSourceInput.getJobName(),
commandSourceInput.getLoanExternalId());
return this.processAndLogCommandService.executeCommand(wrapper, command, true);
}
@@ -249,6 +249,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
} else {
throw new UnsupportedCommandException(wrapper.commandName());
}
} else if (wrapper.isInterestPauseResource() || wrapper.isInterestPauseExternalIdResource()) {
handler = applicationContext.getBean("interestPauseCommandHandler", NewCommandSourceHandler.class);
} else {
handler = commandHandlerProvider.getHandler(wrapper.entityName(), wrapper.actionName());
}
@@ -71,30 +71,34 @@ public final class JsonCommand {
private final Long creditBureauId;
private final Long organisationCreditBureauId;
private final String jobName;
private final ExternalId loanExternalId;
public static JsonCommand from(final String jsonCommand, final JsonElement parsedCommand, final FromJsonHelper fromApiJsonHelper,
final String entityName, final Long resourceId, final Long subresourceId, final Long groupId, final Long clientId,
final Long loanId, final Long savingsId, final String transactionId, final String url, final Long productId,
final Long creditBureauId, final Long organisationCreditBureauId, final String jobName) {
final Long creditBureauId, final Long organisationCreditBureauId, final String jobName, final ExternalId loanExternalId) {
return new JsonCommand(null, jsonCommand, parsedCommand, fromApiJsonHelper, entityName, resourceId, subresourceId, groupId,
clientId, loanId, savingsId, transactionId, url, productId, creditBureauId, organisationCreditBureauId, jobName);
clientId, loanId, savingsId, transactionId, url, productId, creditBureauId, organisationCreditBureauId, jobName,
loanExternalId);
}
public static JsonCommand fromExistingCommand(final Long commandId, final String jsonCommand, final JsonElement parsedCommand,
final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId,
final String url, final Long productId, final Long creditBureauId, final Long organisationCreditBureauId,
final String jobName) {
final String url, final Long productId, final Long creditBureauId, final Long organisationCreditBureauId, final String jobName,
final ExternalId loanExternalId) {
return new JsonCommand(commandId, jsonCommand, parsedCommand, fromApiJsonHelper, entityName, resourceId, subresourceId, null, null,
null, null, null, url, productId, creditBureauId, organisationCreditBureauId, jobName);
null, null, null, url, productId, creditBureauId, organisationCreditBureauId, jobName, loanExternalId);
}
public static JsonCommand fromExistingCommand(final Long commandId, final String jsonCommand, final JsonElement parsedCommand,
final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId,
final Long groupId, final Long clientId, final Long loanId, final Long savingsId, final String transactionId, final String url,
final Long productId, Long creditBureauId, final Long organisationCreditBureauId, final String jobName) {
final Long productId, Long creditBureauId, final Long organisationCreditBureauId, final String jobName,
final ExternalId loanExternalId) {
return new JsonCommand(commandId, jsonCommand, parsedCommand, fromApiJsonHelper, entityName, resourceId, subresourceId, groupId,
clientId, loanId, savingsId, transactionId, url, productId, creditBureauId, organisationCreditBureauId, jobName);
clientId, loanId, savingsId, transactionId, url, productId, creditBureauId, organisationCreditBureauId, jobName,
loanExternalId);
}
@@ -103,7 +107,7 @@ public final class JsonCommand {
return new JsonCommand(command.commandId, jsonCommand, parsedCommand, command.fromApiJsonHelper, command.entityName,
command.resourceId, command.subresourceId, command.groupId, command.clientId, command.loanId, command.savingsId,
command.transactionId, command.url, command.productId, command.creditBureauId, command.organisationCreditBureauId,
command.jobName);
command.jobName, command.loanExternalId);
}
public static JsonCommand fromExistingCommand(JsonCommand command, final JsonElement parsedCommand, final Long clientId) {
@@ -111,13 +115,14 @@ public final class JsonCommand {
return new JsonCommand(command.commandId, jsonCommand, parsedCommand, command.fromApiJsonHelper, command.entityName,
command.resourceId, command.subresourceId, command.groupId, clientId, command.loanId, command.savingsId,
command.transactionId, command.url, command.productId, command.creditBureauId, command.organisationCreditBureauId,
command.jobName);
command.jobName, command.loanExternalId);
}
public JsonCommand(final Long commandId, final String jsonCommand, final JsonElement parsedCommand,
final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId,
final Long groupId, final Long clientId, final Long loanId, final Long savingsId, final String transactionId, final String url,
final Long productId, final Long creditBureauId, final Long organisationCreditBureauId, final String jobName) {
final Long productId, final Long creditBureauId, final Long organisationCreditBureauId, final String jobName,
final ExternalId loanExternalId) {
this.commandId = commandId;
this.jsonCommand = jsonCommand;
@@ -136,6 +141,7 @@ public final class JsonCommand {
this.creditBureauId = creditBureauId;
this.organisationCreditBureauId = organisationCreditBureauId;
this.jobName = jobName;
this.loanExternalId = loanExternalId;
}
public static JsonCommand fromJsonElement(final Long resourceId, final JsonElement parsedCommand) {
@@ -165,6 +171,7 @@ public final class JsonCommand {
this.creditBureauId = null;
this.organisationCreditBureauId = null;
this.jobName = null;
this.loanExternalId = null;
}
public JsonCommand(final Long resourceId, final JsonElement parsedCommand, final FromJsonHelper fromApiJsonHelper) {
@@ -185,10 +192,12 @@ public final class JsonCommand {
this.creditBureauId = null;
this.organisationCreditBureauId = null;
this.jobName = null;
this.loanExternalId = null;
}
public static JsonCommand from(final String jsonCommand) {
return new JsonCommand(null, jsonCommand, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
return new JsonCommand(null, jsonCommand, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null);
}
@@ -49,12 +49,13 @@ public class CommandProcessingResult implements Serializable {
private Boolean rollbackTransaction;
private final ExternalId resourceExternalId;
private final ExternalId subResourceExternalId;
private final ExternalId loanExternalId;
private CommandProcessingResult(final Long commandId, final Long officeId, final Long groupId, final Long clientId, final Long loanId,
final Long savingsId, final String resourceIdentifier, final Long resourceId, final String transactionId,
final Map<String, Object> changes, final Long productId, final Long gsimId, final Long glimId,
final Map<String, Object> creditBureauReportData, Boolean rollbackTransaction, final Long subResourceId,
final ExternalId resourceExternalId, final ExternalId subResourceExternalId) {
final ExternalId resourceExternalId, final ExternalId subResourceExternalId, final ExternalId loanExternalId) {
this.commandId = commandId;
this.officeId = officeId;
this.groupId = groupId;
@@ -73,12 +74,13 @@ public class CommandProcessingResult implements Serializable {
this.subResourceId = subResourceId;
this.resourceExternalId = resourceExternalId;
this.subResourceExternalId = subResourceExternalId;
this.loanExternalId = loanExternalId;
}
protected CommandProcessingResult(final Long resourceId, final Long officeId, final Long commandId, final Map<String, Object> changes,
Long clientId) {
this(commandId, officeId, null, clientId, null, null, resourceId == null ? null : resourceId.toString(), resourceId, null, changes,
null, null, null, null, null, null, ExternalId.empty(), ExternalId.empty());
null, null, null, null, null, null, ExternalId.empty(), ExternalId.empty(), ExternalId.empty());
}
protected CommandProcessingResult(final Long resourceId, final Long officeId, final Long commandId, final Map<String, Object> changes) {
@@ -94,7 +96,7 @@ public class CommandProcessingResult implements Serializable {
commandResult.loanId, commandResult.savingsId, commandResult.resourceIdentifier, resourceId, commandResult.transactionId,
commandResult.changes, commandResult.productId, commandResult.gsimId, commandResult.glimId,
commandResult.creditBureauReportData, commandResult.rollbackTransaction, commandResult.subResourceId,
commandResult.resourceExternalId, commandResult.subResourceExternalId);
commandResult.resourceExternalId, commandResult.subResourceExternalId, commandResult.loanExternalId);
}
public static CommandProcessingResult fromCommandProcessingResult(CommandProcessingResult commandResult) {
@@ -105,10 +107,10 @@ public class CommandProcessingResult implements Serializable {
final Long loanId, final Long savingsId, final String resourceIdentifier, final Long entityId, final Long gsimId,
final Long glimId, final Map<String, Object> creditBureauReportData, final String transactionId,
final Map<String, Object> changes, final Long productId, final Boolean rollbackTransaction, final Long subResourceId,
final ExternalId resourceExternalId, final ExternalId subResourceExternalId) {
final ExternalId resourceExternalId, final ExternalId subResourceExternalId, final ExternalId loanExternalId) {
return new CommandProcessingResult(commandId, officeId, groupId, clientId, loanId, savingsId, resourceIdentifier, entityId,
transactionId, changes, productId, gsimId, glimId, creditBureauReportData, rollbackTransaction, subResourceId,
resourceExternalId, subResourceExternalId);
resourceExternalId, subResourceExternalId, loanExternalId);
}
public static CommandProcessingResult commandOnlyResult(final Long commandId) {
@@ -46,10 +46,13 @@ public class CommandProcessingResultBuilder {
private ExternalId subEntityExternalId = ExternalId.empty();
private ExternalId loanExternalId = ExternalId.empty();
public CommandProcessingResult build() {
return CommandProcessingResult.fromDetails(this.commandId, this.officeId, this.groupId, this.clientId, this.loanId, this.savingsId,
this.resourceIdentifier, this.entityId, this.gsimId, this.glimId, this.creditBureauReportData, this.transactionId,
this.changes, this.productId, this.rollbackTransaction, this.subEntityId, this.entityExternalId, this.subEntityExternalId);
this.changes, this.productId, this.rollbackTransaction, this.subEntityId, this.entityExternalId, this.subEntityExternalId,
this.loanExternalId);
}
public CommandProcessingResultBuilder withCommandId(final Long withCommandId) {
@@ -142,4 +145,8 @@ public class CommandProcessingResultBuilder {
return this;
}
public CommandProcessingResultBuilder withLoanExternalId(final ExternalId loanExternalId) {
this.loanExternalId = loanExternalId;
return this;
}
}
@@ -58,7 +58,7 @@ public class IdempotencyCommandProcessFailedExceptionTest {
public void testInconsistentStatus() {
IdempotentCommandExceptionMapper mapper = new IdempotentCommandExceptionMapper();
CommandWrapper command = new CommandWrapper(null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null);
null, null, null, null, null);
CommandSource source = CommandSource.fullEntryFrom(command, JsonCommand.from("{}"), null, "dummy-key", null);
IdempotentCommandProcessFailedException exception = new IdempotentCommandProcessFailedException(command, null, source);
Response result = mapper.toResponse(exception);
@@ -0,0 +1,117 @@
/**
* 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.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
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.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.interestpauses.data.InterestPauseRequestDto;
import org.apache.fineract.portfolio.interestpauses.data.InterestPauseResponseDto;
import org.apache.fineract.portfolio.interestpauses.service.InterestPauseReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/loans")
@Component
@Tag(name = "Loan Interest Pause", description = "APIs for managing interest pause periods on loans.")
@RequiredArgsConstructor
public class LoanInterestPauseApiResource {
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "LOAN";
private final PlatformSecurityContext context;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final InterestPauseReadPlatformService interestPauseReadPlatformService;
@POST
@Path("/{loanId}/interest-pauses")
@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") })
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);
final CommandWrapper commandRequest = new CommandWrapperBuilder().createInterestPause(loanId).withJson(request.toJson()).build();
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
@POST
@Path("/external-id/{loanExternalId}/interest-pauses")
@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") })
public CommandProcessingResult createInterestPauseByExternalId(
@PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId,
@RequestBody(required = true) final InterestPauseRequestDto request) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
final CommandWrapper commandRequest = new CommandWrapperBuilder().createInterestPauseByExternalId(loanExternalId)
.withJson(request.toJson()).build();
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
@GET
@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") })
public List<InterestPauseResponseDto> retrieveInterestPauses(
@PathParam("loanId") @Parameter(description = "loanId") final Long loanId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
return this.interestPauseReadPlatformService.retrieveInterestPauses(loanId);
}
@GET
@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") })
public List<InterestPauseResponseDto> retrieveInterestPausesByExternalId(
@PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
return this.interestPauseReadPlatformService.retrieveInterestPauses(loanExternalId);
}
}
@@ -0,0 +1,53 @@
/**
* 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.data;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "Request DTO for creating an interest pause")
public class InterestPauseRequestDto {
@Schema(example = "2024-01-01", description = "Start date of the interest pause period")
private String startDate;
@Schema(example = "2024-01-11", description = "End date of the interest pause period")
private String endDate;
@Schema(example = "yyyy-MM-dd", description = "Format of the dates provided")
private String dateFormat;
@Schema(example = "en", description = "Locale to interpret the date format")
private String locale;
public String toJson() {
try {
return new ObjectMapper().writeValueAsString(this);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Error serializing request to JSON", e);
}
}
}
@@ -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.data;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
@Schema(description = "Response DTO for Interest Pause periods")
public class InterestPauseResponseDto {
@Schema(example = "1", description = "ID of the loan term variation")
private Long id;
@Schema(example = "2024-01-01", description = "Start date of the interest pause period")
private LocalDate startDate;
@Schema(example = "2024-01-11", description = "End date of the interest pause period")
private LocalDate endDate;
@Schema(example = "yyyy-MM-dd", description = "Date format used to interpret start and end dates")
private String dateFormat;
@Schema(example = "en", description = "Locale used for date formatting")
private String locale;
}
@@ -0,0 +1,58 @@
/**
* 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.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;
@Component("interestPauseCommandHandler")
@RequiredArgsConstructor
public class InterestPauseCommandHandler 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");
final String locale = command.stringValueOfParameterNamed("locale");
if (command.getLoanId() != null) {
final Long loanId = command.getLoanId();
result = 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);
} 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.service;
import java.util.List;
import org.apache.fineract.portfolio.interestpauses.data.InterestPauseResponseDto;
public interface InterestPauseReadPlatformService {
/**
* Retrieve all interest pause periods for a valid loan.
*
* @param loanId
* @return List of InterestPauseData
*/
List<InterestPauseResponseDto> retrieveInterestPauses(Long loanId);
/**
* Retrieve all interest pause periods for a loan using external ID.
*
* @param loanExternalId
* @return List of InterestPauseData
*/
List<InterestPauseResponseDto> retrieveInterestPauses(String loanExternalId);
}
@@ -0,0 +1,60 @@
/**
* 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.service;
import java.util.List;
import java.util.Locale;
import lombok.AllArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.interestpauses.data.InterestPauseResponseDto;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermVariationsRepository;
import org.springframework.transaction.annotation.Transactional;
@AllArgsConstructor
@Transactional(readOnly = true)
public class InterestPauseReadPlatformServiceImpl implements InterestPauseReadPlatformService {
private final LoanTermVariationsRepository loanTermVariationsRepository;
@Override
public List<InterestPauseResponseDto> retrieveInterestPauses(Long loanId) {
List<LoanTermVariationsData> variations = this.loanTermVariationsRepository.findLoanTermVariationsByLoanIdAndTermType(loanId,
LoanTermVariationType.INTEREST_PAUSE.getValue());
return mapToInterestPauseResponse(variations);
}
@Override
public List<InterestPauseResponseDto> retrieveInterestPauses(String loanExternalId) {
List<LoanTermVariationsData> variations = this.loanTermVariationsRepository.findLoanTermVariationsByExternalLoanIdAndTermType(
new ExternalId(loanExternalId), LoanTermVariationType.INTEREST_PAUSE.getValue());
return mapToInterestPauseResponse(variations);
}
private List<InterestPauseResponseDto> mapToInterestPauseResponse(List<LoanTermVariationsData> variations) {
return variations.stream()
.map(variation -> new InterestPauseResponseDto(variation.getId(), variation.getTermVariationApplicableFrom(),
variation.getDateValue(), DateUtils.DEFAULT_DATE_FORMAT, Locale.getDefault().toString()))
.toList();
}
}
@@ -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.service;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
public interface InterestPauseWritePlatformService {
CommandProcessingResult createInterestPause(ExternalId loanExternalId, String startDate, String endDate, String dateFormat,
String locale);
/**
* Create a new interest pause period for a loan identified by its internal ID.
*
* @param loanId
* the 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)
* @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);
}
@@ -0,0 +1,136 @@
/**
* 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.service;
import java.time.LocalDate;
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.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;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
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.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.springframework.transaction.annotation.Transactional;
@AllArgsConstructor
@Transactional
public class InterestPauseWritePlatformServiceImpl implements InterestPauseWritePlatformService {
private final LoanTermVariationsRepository loanTermVariationsRepository;
private final LoanRepositoryWrapper loanRepositoryWrapper;
@Override
public CommandProcessingResult createInterestPause(ExternalId loanExternalId, String startDateString, String endDateString,
String dateFormat, String locale) {
final LocalDate startDate = parseDate(startDateString, dateFormat, locale);
final LocalDate endDate = parseDate(endDateString, dateFormat, locale);
return processInterestPause(() -> loanRepositoryWrapper.findOneWithNotFoundDetection(loanExternalId), startDate, endDate,
dateFormat, locale);
}
@Override
public CommandProcessingResult createInterestPause(Long loanId, String startDateString, String endDateString, String dateFormat,
String locale) {
final LocalDate startDate = parseDate(startDateString, dateFormat, locale);
final LocalDate endDate = parseDate(endDateString, dateFormat, locale);
return processInterestPause(() -> loanRepositoryWrapper.findOneWithNotFoundDetection(loanId), startDate, endDate, dateFormat,
locale);
}
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 savedVariation = loanTermVariationsRepository.saveAndFlush(variation);
return new CommandProcessingResultBuilder().withEntityId(savedVariation.getId()).build();
}
private void validateInterestPauseDates(Loan loan, LocalDate startDate, LocalDate endDate, String dateFormat, String locale) {
validateOrThrow(baseDataValidator -> {
baseDataValidator.reset().parameter("startDate").value(startDate).notBlank();
baseDataValidator.reset().parameter("endDate").value(endDate).notBlank();
baseDataValidator.reset().parameter("dateFormat").value(dateFormat).notBlank();
baseDataValidator.reset().parameter("locale").value(locale).notBlank();
});
if (startDate.isBefore(loan.getSubmittedOnDate())) {
throw new GeneralPlatformDomainRuleException("interest.pause.start.date.before.loan.start.date",
String.format("Interest pause start date (%s) cannot be earlier than loan start date (%s).", startDate,
loan.getSubmittedOnDate()),
startDate, loan.getSubmittedOnDate());
}
if (endDate.isAfter(loan.getMaturityDate())) {
throw new GeneralPlatformDomainRuleException("interest.pause.end.date.after.loan.maturity.date", String
.format("Interest pause end date (%s) cannot be later than loan maturity date (%s).", endDate, loan.getMaturityDate()),
endDate, loan.getMaturityDate());
}
if (endDate.isBefore(startDate)) {
throw new GeneralPlatformDomainRuleException("interest.pause.end.date.before.start.date", String
.format("Interest pause end date (%s) must not be before the interest pause start date (%s).", endDate, startDate),
endDate, startDate);
}
}
private LocalDate parseDate(String date, String dateFormat, String locale) {
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat, Locale.forLanguageTag(locale));
return LocalDate.parse(date, formatter);
} catch (DateTimeParseException e) {
throw new PlatformApiDataValidationException("validation.msg.invalid.date.format",
String.format("Invalid date format. Provided: %s, Expected format: %s, Locale: %s", date, dateFormat, locale),
e.getMessage(), e);
}
}
private void validateOrThrow(Consumer<DataValidatorBuilder> baseDataValidator) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors).resource("InterestPause");
baseDataValidator.accept(dataValidatorBuilder);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
}
@@ -113,6 +113,8 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
+ "and (:futureCharges = true or ls.fromDate < :tillDate or (ls.installmentNumber = (select min(lsi.installmentNumber) from LoanRepaymentScheduleInstallment lsi where lsi.loan.id = l.id and lsi.isDownPayment = false) and ls.fromDate = :tillDate))))";
String FIND_LOANS_FOR_ADD_ACCRUAL = LOANS_FOR_ACCRUAL + "and (:futureCharges = true or ls.dueDate <= :tillDate)))";
String FIND_LOAN_BY_EXTERNAL_ID = "SELECT loan FROM Loan loan WHERE loan.externalId = :externalId";
@Query(FIND_GROUP_LOANS_DISBURSED_AFTER)
List<Loan> getGroupLoansDisbursedAfter(@Param("disbursementDate") LocalDate disbursementDate, @Param("groupId") Long groupId,
@Param("loanType") Integer loanType);
@@ -247,4 +249,7 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
@Query(FIND_LOANS_FOR_ADD_ACCRUAL)
List<Loan> findLoansForAddAccrual(@Param("accountingType") Integer accountingType, @Param("tillDate") LocalDate tillDate,
@Param("futureCharges") boolean futureCharges);
@Query(FIND_LOAN_BY_EXTERNAL_ID)
Optional<Loan> findByExternalId(@Param("externalId") ExternalId externalId);
}
@@ -49,6 +49,20 @@ public class LoanRepositoryWrapper {
return this.findOneWithNotFoundDetection(id, false);
}
@Transactional(readOnly = true)
public Loan findOneWithNotFoundDetection(final ExternalId externalId) {
return this.findOneWithNotFoundDetection(externalId, false);
}
@Transactional(readOnly = true)
public Loan findOneWithNotFoundDetection(final ExternalId externalId, boolean loadLazyCollections) {
final Loan loan = this.repository.findByExternalId(externalId).orElseThrow(() -> new LoanNotFoundException(externalId));
if (loadLazyCollections) {
loan.initializeLazyCollections();
}
return loan;
}
@Transactional(readOnly = true)
public Loan findOneWithNotFoundDetection(final Long id, boolean loadLazyCollections) {
final Loan loan = this.repository.findById(id).orElseThrow(() -> new LoanNotFoundException(id));
@@ -30,7 +30,8 @@ public enum LoanTermVariationType {
GRACE_ON_INTEREST(7, "loanTermType.graceOnInterest"), //
GRACE_ON_PRINCIPAL(8, "loanTermType.graceOnPrincipal"), //
EXTEND_REPAYMENT_PERIOD(9, "loanTermType.extendRepaymentPeriod"), //
INTEREST_RATE_FROM_INSTALLMENT(10, "loanTermType.interestRateFromInstallment"); //
INTEREST_RATE_FROM_INSTALLMENT(10, "loanTermType.interestRateFromInstallment"), //
INTEREST_PAUSE(11, "loanTermType.interestPause"); //
private final Integer value;
private final String code;
@@ -82,6 +83,9 @@ public enum LoanTermVariationType {
case 10:
enumeration = LoanTermVariationType.INTEREST_RATE_FROM_INSTALLMENT;
break;
case 11:
enumeration = LoanTermVariationType.INTEREST_PAUSE;
break;
}
return enumeration;
}
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -49,4 +50,14 @@ public interface LoanTermVariationsRepository
""")
List<LoanTermVariationsData> findLoanTermVariationsByLoanIdAndTermType(@Param("loanId") long loanId, @Param("termType") int termType);
@Query("""
select new org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData(
ltv.id, ltv.termType, ltv.termApplicableFrom, ltv.decimalValue, ltv.dateValue, ltv.isSpecificToInstallment
)
from LoanTermVariations ltv
where ltv.loan.externalId = :loanExternalId and ltv.termType = :termType
order by ltv.termApplicableFrom
""")
List<LoanTermVariationsData> findLoanTermVariationsByExternalLoanIdAndTermType(@Param("loanExternalId") ExternalId loanExternalId,
@Param("termType") int termType);
}
@@ -230,7 +230,7 @@ class AdvancedPaymentAllocationsJsonParserTest {
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonMap);
JsonCommand command = JsonCommand.from(json, JsonParser.parseString(json), fromJsonHelper, null, 1L, 2L, 3L, 4L, null, null, null,
null, null, null, null, null);
null, null, null, null, null, null);
return command;
}
@@ -118,7 +118,7 @@ public class CreditAllocationsJsonParserTest {
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonMap);
JsonCommand command = JsonCommand.from(json, JsonParser.parseString(json), fromJsonHelper, null, 1L, 2L, 3L, 4L, null, null, null,
null, null, null, null, null);
null, null, null, null, null, null);
return command;
}
@@ -268,7 +268,7 @@ public class CenterImportHandler implements ImportHandler {
String payload = gsonBuilder.create().toJson(calendarData);
CommandWrapper commandWrapper = new CommandWrapper(result.getOfficeId(), result.getGroupId(), result.getClientId(),
result.getLoanId(), result.getSavingsId(), null, null, null, null, null, payload, result.getTransactionId(),
result.getProductId(), null, null, null, null, idempotencyKeyGenerator.create());
result.getProductId(), null, null, null, null, idempotencyKeyGenerator.create(), null);
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
.createCalendar(commandWrapper, TemplatePopulateImportConstants.CENTER_ENTITY_TYPE, result.getGroupId()) //
.withJson(payload) //
@@ -246,7 +246,7 @@ public class GroupImportHandler implements ImportHandler {
String payload = gsonBuilder.create().toJson(calendarData);
CommandWrapper commandWrapper = new CommandWrapper(result.getOfficeId(), result.getGroupId(), result.getClientId(),
result.getLoanId(), result.getSavingsId(), null, null, null, null, null, payload, result.getTransactionId(),
result.getProductId(), null, null, null, null, idempotencyKeyGenerator.create());
result.getProductId(), null, null, null, null, idempotencyKeyGenerator.create(), null);
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
.createCalendar(commandWrapper, TemplatePopulateImportConstants.CENTER_ENTITY_TYPE, result.getGroupId()) //
.withJson(payload) //
@@ -429,7 +429,8 @@ public class ThitsaWorksCreditBureauIntegrationWritePlatformServiceImpl implemen
JsonCommand apicommand = JsonCommand.from(json, parsedCommand, this.fromApiJsonHelper, wrapper.getEntityName(),
wrapper.getEntityId(), wrapper.getSubentityId(), wrapper.getGroupId(), wrapper.getClientId(), wrapper.getLoanId(),
wrapper.getSavingsId(), wrapper.getTransactionId(), wrapper.getHref(), wrapper.getProductId(),
wrapper.getCreditBureauId(), wrapper.getOrganisationCreditBureauId(), wrapper.getJobName());
wrapper.getCreditBureauId(), wrapper.getOrganisationCreditBureauId(), wrapper.getJobName(),
wrapper.getLoanExternalId());
this.fromApiJsonDeserializer.validateForCreate(apicommand.json());
@@ -37,7 +37,7 @@ public class InteropWrapperBuilder {
public CommandWrapper build() {
return new CommandWrapper(null, null, null, null, null, actionName, entityName, null, null, href, json, null, null, null, null,
null, null, null);
null, null, null, null);
}
public InteropWrapperBuilder withJson(final String json) {
@@ -784,7 +784,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(overdueInstallment.toString());
final JsonCommand command = JsonCommand.from(overdueInstallment.toString(), parsedCommand, this.fromApiJsonHelper, null, null,
null, null, null, loanId, null, null, null, null, null, null, null);
null, null, null, loanId, null, null, null, null, null, null, null, null);
LoanOverdueDTO overdueDTO = applyChargeToOverdueLoanInstallment(loan, overdueInstallment.getChargeId(),
overdueInstallment.getPeriodNumber(), command);
loan = overdueDTO.getLoan();
@@ -64,6 +64,10 @@ import org.apache.fineract.portfolio.fund.domain.FundRepository;
import org.apache.fineract.portfolio.fund.service.FundReadPlatformService;
import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper;
import org.apache.fineract.portfolio.group.service.GroupReadPlatformService;
import org.apache.fineract.portfolio.interestpauses.service.InterestPauseReadPlatformService;
import org.apache.fineract.portfolio.interestpauses.service.InterestPauseReadPlatformServiceImpl;
import org.apache.fineract.portfolio.interestpauses.service.InterestPauseWritePlatformService;
import org.apache.fineract.portfolio.interestpauses.service.InterestPauseWritePlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository;
@@ -81,6 +85,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanSchedu
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanChargeMapper;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanCollateralManagementMapper;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermVariationsRepository;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator;
@@ -456,4 +461,17 @@ public class LoanAccountConfiguration {
public LoanRefundService loanRefundService(LoanRefundValidator loanRefundValidator) {
return new LoanRefundService(loanRefundValidator);
}
@Bean
@ConditionalOnMissingBean(InterestPauseReadPlatformService.class)
public InterestPauseReadPlatformService interestPauseReadPlatformService(LoanTermVariationsRepository loanTermVariationsRepository) {
return new InterestPauseReadPlatformServiceImpl(loanTermVariationsRepository);
}
@Bean
@ConditionalOnMissingBean(InterestPauseWritePlatformService.class)
public InterestPauseWritePlatformService interestPauseWritePlatformService(LoanTermVariationsRepository loanTermVariationsRepository,
LoanRepositoryWrapper loanRepositoryWrapper) {
return new InterestPauseWritePlatformServiceImpl(loanTermVariationsRepository, loanRepositoryWrapper);
}
}
@@ -38,7 +38,7 @@ public class ShareAccountCommandsServiceImpl implements AccountsCommandsService
public Object handleCommand(Long accountId, String command, String jsonBody) {
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(jsonBody);
final JsonCommand jsonCommand = JsonCommand.from(jsonBody, parsedCommand, this.fromApiJsonHelper, null, null, null, null, null,
null, null, null, null, null, null, null, null);
null, null, null, null, null, null, null, null, null);
if (ShareAccountApiConstants.APPROVE_COMMAND.equals(command)) {
return approveShareAccount(accountId, jsonCommand);
}
@@ -39,7 +39,7 @@ public class ShareProductCommandsServiceImpl implements ProductCommandsService {
public Object handleCommand(Long productId, String command, String jsonBody) {
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(jsonBody);
final JsonCommand jsonCommand = JsonCommand.from(jsonBody, parsedCommand, this.fromApiJsonHelper, null, null, null, null, null,
null, null, null, null, null, null, null, null);
null, null, null, null, null, null, null, null, null);
if (ShareProductApiConstants.PREIEW_DIVIDENDS_COMMAND_STRING.equals(command)) {
return null;
} else if (ShareProductApiConstants.POST_DIVIDENdS_COMMAND_STRING.equals(command)) {
@@ -179,4 +179,5 @@
<include file="parts/0158_create_acc_gl_journal_entry_annual_summary.xml" relativeToChangelogFile="true" />
<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" />
</databaseChangeLog>
@@ -0,0 +1,33 @@
<?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_portfolio_command_source">
<column name="loan_external_id" type="VARCHAR(100)">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -41,8 +41,8 @@ public class CommandHandlerProviderStepDefinitions implements En {
});
When("The user processes the command with ID {long}", (Long id) -> {
this.result = commandHandler
.processCommand(JsonCommand.fromExistingCommand(id, null, null, null, null, null, null, null, null, null, null, null));
this.result = commandHandler.processCommand(
JsonCommand.fromExistingCommand(id, null, null, null, null, null, null, null, null, null, null, null, null));
});
Then("The command ID matches {long}", (Long id) -> {
@@ -101,7 +101,7 @@ public class CommandServiceStepDefinitions implements En {
public DummyCommand() {
super(null, null, null, null, null, null, null, null, null, null, "{}", null, null, null, null, null, null,
UUID.randomUUID().toString());
UUID.randomUUID().toString(), null);
}
@Override
@@ -124,7 +124,7 @@ public class CommandServiceStepDefinitions implements En {
JsonCommand command = JsonCommand.from(json, null, null, wrapper.getEntityName(), wrapper.getEntityId(),
wrapper.getSubentityId(), wrapper.getGroupId(), wrapper.getClientId(), wrapper.getLoanId(), wrapper.getSavingsId(),
wrapper.getTransactionId(), wrapper.getHref(), wrapper.getProductId(), wrapper.getCreditBureauId(),
wrapper.getOrganisationCreditBureauId(), wrapper.getJobName());
wrapper.getOrganisationCreditBureauId(), wrapper.getJobName(), wrapper.getLoanExternalId());
return this.processAndLogCommandService.executeCommand(wrapper, command, true);
}
@@ -77,7 +77,7 @@ public class IdempotencyKeyResolverTest {
public void testIPKResolveFromWrapper() {
String idk = "idk";
CommandWrapper wrapper = new CommandWrapper(null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, idk);
null, null, null, idk, null);
String resolvedIdk = underTest.resolve(wrapper);
Assertions.assertEquals(idk, resolvedIdk);
}
@@ -421,7 +421,7 @@ public class ThitsaWorksCreditBureauIntegrationWritePlatformServiceImplTest {
command.put("creditBureauID", "1"); // Must match to the mocked config
String json = mapper.writeValueAsString(command);
return JsonCommand.from(json, JsonParser.parseString(json), fromJsonHelper, null, 1L, 2L, 3L, 4L, null, null, null, null, null,
null, null, null);
null, null, null, null);
}
private void mockTokenGeneration() {
@@ -0,0 +1,315 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.integrationtests;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
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.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.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.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Slf4j
@ExtendWith({ LoanTestLifecycleExtension.class })
public class LoanInterestPauseApiTest extends BaseLoanIntegrationTest {
private static final Logger LOG = LoggerFactory.getLogger(LoanInterestPauseApiTest.class);
private static RequestSpecification REQUEST_SPEC;
private static ResponseSpecification RESPONSE_SPEC;
private static ResponseSpecification RESPONSE_SPEC_403;
private static LoanTransactionHelper LOAN_TRANSACTIONAL_HELPER;
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 String externalId;
private static String nonExistExternalId = "7c4fb86f-a778-4d02-b7a8-ec3ec98941fa";
@BeforeAll
public static void setupTests() {
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();
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();
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();
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");
}
@Test
public void testCreateInterestPauseByLoanId_validRequest_shouldSucceed() {
PostLoansLoanIdTransactionsResponse response = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByLoanId("2023-01-01", "2023-02-05",
"yyyy-MM-dd", "en", loanId);
Assertions.assertNotNull(response);
Assertions.assertNotNull(response.getResourceId());
}
@Test
public void testCreateInterestPauseByLoanId_endDateBeforeStartDate_shouldFail() {
try {
LOAN_TRANSACTION_HELPER_403.createInterestPauseByLoanId("2024-12-05", "2024-12-01", "yyyy-MM-dd", "en", loanId);
} catch (Exception e) {
String responseBody = e.getMessage();
Assertions.assertNotNull(responseBody, "Response body should not be null");
Assertions.assertTrue(responseBody.contains("interest.pause.end.date.before.start.date"),
"Response should contain the validation error message for end date before start date");
}
}
@Test
public void testCreateInterestPauseByLoanId_startDateBeforeLoanStart_shouldFail() {
try {
LOAN_TRANSACTION_HELPER_403.createInterestPauseByLoanId("2022-12-01", "2024-12-05", "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");
}
}
@Test
public void testCreateInterestPauseByLoanId_endDateAfterLoanMaturity_shouldFail() {
try {
LOAN_TRANSACTION_HELPER_403.createInterestPauseByLoanId("2024-12-01", "2025-12-05", "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.after.loan.maturity.date"),
"Response should contain the validation error message for end date after loan maturity date");
}
}
@Test
public void testRetrieveInterestPausesByLoanId_noPauses_shouldReturnEmpty() {
String response = LOAN_TRANSACTIONAL_HELPER.retrieveInterestPauseByLoanId(nonExistLoanId);
Assertions.assertNotNull(response, "Response should not be null");
Assertions.assertFalse(response.contains("id"));
Assertions.assertFalse(response.contains("startDate"));
Assertions.assertFalse(response.contains("endDate"));
}
@Test
public void testRetrieveInterestPausesByLoanId_shouldReturnData() {
LOAN_TRANSACTIONAL_HELPER.createInterestPauseByLoanId("2023-01-01", "2023-02-05", "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"));
}
@Test
public void testCreateInterestPauseByExternalLoanId_validRequest_shouldSucceed() {
PostLoansLoanIdTransactionsResponse response = LOAN_TRANSACTIONAL_HELPER.createInterestPauseByExternalId("2023-01-01", "2023-02-05",
"yyyy-MM-dd", "en", externalId);
Assertions.assertNotNull(response);
Assertions.assertNotNull(response.getResourceId());
}
@Test
public void testCreateInterestPauseByExternalLoanId_endDateBeforeStartDate_shouldFail() {
try {
LOAN_TRANSACTION_HELPER_403.createInterestPauseByExternalId("2024-12-05", "2024-12-01", "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 testCreateInterestPauseByExternalLoanId_startDateBeforeLoanStart_shouldFail() {
try {
LOAN_TRANSACTION_HELPER_403.createInterestPauseByExternalId("2022-12-01", "2024-12-05", "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 testCreateInterestPauseByExternalLoanId_endDateAfterLoanMaturity_shouldFail() {
try {
LOAN_TRANSACTION_HELPER_403.createInterestPauseByExternalId("2024-12-01", "2025-12-05", "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.after.loan.maturity.date"),
"Response should contain the validation error message for end date after loan maturity date");
}
}
@Test
public void testRetrieveInterestPausesByExternalLoanId_noPauses_shouldReturnEmpty() {
String response = LOAN_TRANSACTIONAL_HELPER.retrieveInterestPauseByExternalId(nonExistExternalId);
Assertions.assertNotNull(response, "Response should not be null");
Assertions.assertFalse(response.contains("id"));
Assertions.assertFalse(response.contains("startDate"));
Assertions.assertFalse(response.contains("endDate"));
}
@Test
public void testRetrieveInterestPausesByExternalLoanId_shouldReturnData() {
LOAN_TRANSACTIONAL_HELPER.createInterestPauseByExternalId("2023-01-01", "2023-02-05", "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"));
}
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);
}
private static AdvancedPaymentData createDefaultPaymentAllocation() {
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
advancedPaymentData.setTransactionType("DEFAULT");
advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT");
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);
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
return advancedPaymentData;
}
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());
}
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);
}
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);
}
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)));
}
}
@@ -116,6 +116,7 @@ public class LoanTransactionHelper extends IntegrationTest {
private static final String WRITE_OFF_LOAN_COMMAND = "writeoff";
private static final String WAIVE_INTEREST_COMMAND = "waiveinterest";
private static final String MAKE_REPAYMENT_COMMAND = "repayment";
private static final String INTEREST_PAUSE_COMMAND = "interestpause";
private static final String UNDO = "undo";
private static final String LOANCHARGE_REFUND_REPAYMENT_COMMAND = "chargeRefund";
private static final String CREDIT_BALANCE_REFUND_COMMAND = "creditBalanceRefund";
@@ -607,6 +608,34 @@ public class LoanTransactionHelper extends IntegrationTest {
return ok(newFineract(user, pass).loanTransactions.executeLoanTransaction(loanId, request, "repayment"));
}
public PostLoansLoanIdTransactionsResponse createInterestPauseByLoanId(final String startDate, final String endDate,
final String dateFormat, final String locale, final Long 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);
return postLoanTransaction(createInterestPause(INTEREST_PAUSE_COMMAND, loanID), body);
}
public PostLoansLoanIdTransactionsResponse createInterestPauseByExternalId(final String startDate, final String endDate,
final String dateFormat, final String locale, final String externalId) {
log.info("Creating interest pause for Loan {} from {} to {} with dateFormat {} and locale {}", externalId, startDate, endDate,
dateFormat, locale);
String body = getInterestPauseBodyAsJSON(startDate, endDate, dateFormat, locale);
return postLoanTransaction(createInterestPause(INTEREST_PAUSE_COMMAND, externalId), body);
}
public String retrieveInterestPauseByLoanId(final Long loanID) {
log.info("Retrieving interest pauses for Loan ID {}", loanID);
String url = retrieveInterestPause(loanID);
return Utils.performServerGet(requestSpec, responseSpec, url);
}
public String retrieveInterestPauseByExternalId(final String externalId) {
log.info("Retrieving interest pauses for External ID {}", externalId);
String url = retrieveInterestPause(externalId);
return Utils.performServerGet(requestSpec, responseSpec, url);
}
public PostLoansLoanIdTransactionsResponse makeInterestPaymentWaiver(final Long loanId,
final PostLoansLoanIdTransactionsRequest request) {
return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "interestPaymentWaiver"));
@@ -1218,6 +1247,16 @@ public class LoanTransactionHelper extends IntegrationTest {
return new Gson().toJson(map);
}
private String getInterestPauseBodyAsJSON(final String startDate, final String endDate, final String dateFormat, final String locale) {
final HashMap<String, String> map = new HashMap<>();
map.put("startDate", startDate);
map.put("endDate", endDate);
map.put("dateFormat", dateFormat);
map.put("locale", locale);
return new Gson().toJson(map);
}
private String getAdjustTransactionJsonBody(String date, String amount) {
final HashMap<String, String> map = new HashMap<>();
map.put("transactionDate", date);
@@ -1411,6 +1450,23 @@ 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) {
return "/fineract-provider/api/v1/loans/" + loanID + "/interest-pauses?command=" + command + "&" + Utils.TENANT_IDENTIFIER;
}
private String createInterestPause(final String command, final String externalId) {
return "/fineract-provider/api/v1/loans/external-id/" + externalId + "/interest-pauses?command=" + command + "&"
+ Utils.TENANT_IDENTIFIER;
}
private String retrieveInterestPause(final Long loanID) {
return "/fineract-provider/api/v1/loans/" + loanID + "/interest-pauses?" + Utils.TENANT_IDENTIFIER;
}
private String retrieveInterestPause(final String externalId) {
return "/fineract-provider/api/v1/loans/external-id/" + externalId + "/interest-pauses?" + Utils.TENANT_IDENTIFIER;
}
private String createInteroperationLoanTransactionURL(final String accountNo) {
return "/fineract-provider/api/v1/interoperation/transactions/" + accountNo + "/loanrepayment";
}