diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java index 97b4cbbe142..ad28f696c2b 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java @@ -133,6 +133,9 @@ public class CommandSource extends AbstractPersistableCustom { @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 static CommandSource fullEntryFrom(final CommandWrapper wrapper, final Js .transactionId(command.getTransactionId()) // .creditBureauId(command.getCreditBureauId()) // .organisationCreditBureauId(command.getOrganisationCreditBureauId()) // + .loanExternalId(command.getLoanExternalId()) // .build(); // } @@ -195,5 +199,6 @@ public void updateForAudit(final CommandProcessingResult result) { this.resourceExternalId = result.getResourceExternalId(); this.subResourceId = result.getSubResourceId(); this.subResourceExternalId = result.getSubResourceExternalId(); + this.loanExternalId = result.getLoanExternalId(); } } diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandWrapper.java b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandWrapper.java index 135c1bce64b..5b51f40de3a 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandWrapper.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandWrapper.java @@ -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 static CommandWrapper fromExistingCommand(final Long commandId, final Str 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 @@ private CommandWrapper(final Long commandId, final String actionName, final Stri 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 CommandWrapper(final Long officeId, final Long groupId, final Long client 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 @@ private CommandWrapper(final Long commandId, final String actionName, final Stri this.organisationCreditBureauId = organisationCreditBureauId; this.jobName = null; this.idempotencyKey = idempotencyKey; + this.loanExternalId = loanExternalId; } public boolean isCreate() { @@ -240,6 +247,14 @@ public boolean isPasswordPreferencesResource() { 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; } diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index dad66a8611b..497862cbffe 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -21,6 +21,7 @@ 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 CommandWrapperBuilder createDelinquencyAction(final Long loanId) { 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; + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java index 1de67b6e879..dd2d8c81bff 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java @@ -73,7 +73,7 @@ public CommandProcessingResult logCommandSource(final CommandWrapper wrapper) { 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 CommandProcessingResult approveEntry(final Long makerCheckerId) { 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); } diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java index 3d5917d9df6..9b525d5cd9c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java @@ -249,6 +249,8 @@ private NewCommandSourceHandler findCommandHandler(final CommandWrapper wrapper) } else { throw new UnsupportedCommandException(wrapper.commandName()); } + } else if (wrapper.isInterestPauseResource() || wrapper.isInterestPauseExternalIdResource()) { + handler = applicationContext.getBean("interestPauseCommandHandler", NewCommandSourceHandler.class); } else { handler = commandHandlerProvider.getHandler(wrapper.entityName(), wrapper.actionName()); } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java index 46f9f2ddb26..72ad7c27d08 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java @@ -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 static JsonCommand fromExistingCommand(JsonCommand command, final JsonEle 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 static JsonCommand fromExistingCommand(JsonCommand command, final JsonEle 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 JsonCommand(final Long commandId, final String jsonCommand, final JsonEle 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 JsonCommand(final Long resourceId, final JsonElement parsedCommand) { 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 JsonCommand(final Long resourceId, final JsonElement parsedCommand, final 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); } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java index 8be8de3888a..5b07f76bf2a 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java @@ -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 changes, final Long productId, final Long gsimId, final Long glimId, final Map 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 @@ private CommandProcessingResult(final Long commandId, final Long officeId, final 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 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 changes) { @@ -94,7 +96,7 @@ public static CommandProcessingResult fromCommandProcessingResult(CommandProcess 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 static CommandProcessingResult fromDetails(final Long commandId, final Lo final Long loanId, final Long savingsId, final String resourceIdentifier, final Long entityId, final Long gsimId, final Long glimId, final Map creditBureauReportData, final String transactionId, final Map 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) { diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java index 1536f08be42..e630ba7076a 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java @@ -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 CommandProcessingResultBuilder withSubEntityExternalId(final ExternalId s return this; } + public CommandProcessingResultBuilder withLoanExternalId(final ExternalId loanExternalId) { + this.loanExternalId = loanExternalId; + return this; + } } diff --git a/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exception/IdempotencyCommandProcessFailedExceptionTest.java b/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exception/IdempotencyCommandProcessFailedExceptionTest.java index 04ba9aa541f..d246f833b6d 100644 --- a/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exception/IdempotencyCommandProcessFailedExceptionTest.java +++ b/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exception/IdempotencyCommandProcessFailedExceptionTest.java @@ -58,7 +58,7 @@ public void tearDown() { 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); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/api/LoanInterestPauseApiResource.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/api/LoanInterestPauseApiResource.java new file mode 100644 index 00000000000..1b08ad6d8d9 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/api/LoanInterestPauseApiResource.java @@ -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 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 retrieveInterestPausesByExternalId( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId) { + + this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); + + return this.interestPauseReadPlatformService.retrieveInterestPauses(loanExternalId); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/data/InterestPauseRequestDto.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/data/InterestPauseRequestDto.java new file mode 100644 index 00000000000..77efbbcb946 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/data/InterestPauseRequestDto.java @@ -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); + } + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/data/InterestPauseResponseDto.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/data/InterestPauseResponseDto.java new file mode 100644 index 00000000000..63578cf9dfd --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/data/InterestPauseResponseDto.java @@ -0,0 +1,45 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.interestpauses.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; +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/handler/InterestPauseCommandHandler.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/handler/InterestPauseCommandHandler.java new file mode 100644 index 00000000000..5747625dd22 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/handler/InterestPauseCommandHandler.java @@ -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; + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseReadPlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseReadPlatformService.java new file mode 100644 index 00000000000..a55326b3edc --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseReadPlatformService.java @@ -0,0 +1,41 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.interestpauses.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 retrieveInterestPauses(Long loanId); + + /** + * Retrieve all interest pause periods for a loan using external ID. + * + * @param loanExternalId + * @return List of InterestPauseData + */ + List retrieveInterestPauses(String loanExternalId); +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseReadPlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseReadPlatformServiceImpl.java new file mode 100644 index 00000000000..e8ae87d7537 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseReadPlatformServiceImpl.java @@ -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 retrieveInterestPauses(Long loanId) { + List variations = this.loanTermVariationsRepository.findLoanTermVariationsByLoanIdAndTermType(loanId, + LoanTermVariationType.INTEREST_PAUSE.getValue()); + + return mapToInterestPauseResponse(variations); + } + + @Override + public List retrieveInterestPauses(String loanExternalId) { + List variations = this.loanTermVariationsRepository.findLoanTermVariationsByExternalLoanIdAndTermType( + new ExternalId(loanExternalId), LoanTermVariationType.INTEREST_PAUSE.getValue()); + + return mapToInterestPauseResponse(variations); + } + + private List mapToInterestPauseResponse(List variations) { + return variations.stream() + .map(variation -> new InterestPauseResponseDto(variation.getId(), variation.getTermVariationApplicableFrom(), + variation.getDateValue(), DateUtils.DEFAULT_DATE_FORMAT, Locale.getDefault().toString())) + .toList(); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformService.java new file mode 100644 index 00000000000..e3b8c397a22 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformService.java @@ -0,0 +1,41 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.interestpauses.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); +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformServiceImpl.java new file mode 100644 index 00000000000..272ba3f9ff6 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformServiceImpl.java @@ -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 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 baseDataValidator) { + final List 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); + } + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java index 0ddebb1a68c..4db8dedf835 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java @@ -113,6 +113,8 @@ public interface LoanRepository extends JpaRepository, 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 getGroupLoansDisbursedAfter(@Param("disbursementDate") LocalDate disbursementDate, @Param("groupId") Long groupId, @Param("loanType") Integer loanType); @@ -247,4 +249,7 @@ List findLoansForPeriodicAccrual(@Param("accountingType") Integer accounti @Query(FIND_LOANS_FOR_ADD_ACCRUAL) List findLoansForAddAccrual(@Param("accountingType") Integer accountingType, @Param("tillDate") LocalDate tillDate, @Param("futureCharges") boolean futureCharges); + + @Query(FIND_LOAN_BY_EXTERNAL_ID) + Optional findByExternalId(@Param("externalId") ExternalId externalId); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java index 3c0f7526e01..bac7cb5b4ee 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java @@ -49,6 +49,20 @@ public Loan findOneWithNotFoundDetection(final Long id) { 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)); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariationType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariationType.java index 7a462c00458..21e4655d388 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariationType.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariationType.java @@ -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 static LoanTermVariationType fromInt(final Integer value) { case 10: enumeration = LoanTermVariationType.INTEREST_RATE_FROM_INSTALLMENT; break; + case 11: + enumeration = LoanTermVariationType.INTEREST_PAUSE; + break; } return enumeration; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanTermVariationsRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanTermVariationsRepository.java index d667825e9e6..0bf7209fff8 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanTermVariationsRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanTermVariationsRepository.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain; import java.util.List; +import 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 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 findLoanTermVariationsByExternalLoanIdAndTermType(@Param("loanExternalId") ExternalId loanExternalId, + @Param("termType") int termType); } diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/AdvancedPaymentAllocationsJsonParserTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/AdvancedPaymentAllocationsJsonParserTest.java index 85b30ee95aa..dba4bc6f5a5 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/AdvancedPaymentAllocationsJsonParserTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/AdvancedPaymentAllocationsJsonParserTest.java @@ -230,7 +230,7 @@ private JsonCommand createJsonCommand(Map jsonMap) throws JsonPr 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; } diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/CreditAllocationsJsonParserTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/CreditAllocationsJsonParserTest.java index d33aa1fe84e..45138580bf6 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/CreditAllocationsJsonParserTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/CreditAllocationsJsonParserTest.java @@ -118,7 +118,7 @@ private JsonCommand createJsonCommand(Map jsonMap) throws JsonPr 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; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/center/CenterImportHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/center/CenterImportHandler.java index 42878e29662..40bfb22d8a0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/center/CenterImportHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/center/CenterImportHandler.java @@ -268,7 +268,7 @@ private Integer importCenterMeeting(final List meetings, final Com 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) // diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/group/GroupImportHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/group/GroupImportHandler.java index 2fa48085a75..68558fa79df 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/group/GroupImportHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/group/GroupImportHandler.java @@ -246,7 +246,7 @@ private Integer importGroupMeeting(final List meetings, CommandPro 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) // diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/service/ThitsaWorksCreditBureauIntegrationWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/service/ThitsaWorksCreditBureauIntegrationWritePlatformServiceImpl.java index 8316d71bd79..3a3a2abcf4f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/service/ThitsaWorksCreditBureauIntegrationWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/creditbureau/service/ThitsaWorksCreditBureauIntegrationWritePlatformServiceImpl.java @@ -429,7 +429,8 @@ public CreditBureauToken createToken(Long bureauID) { 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()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/api/InteropWrapperBuilder.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/api/InteropWrapperBuilder.java index 20a7c2bc553..6556b50c24c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/api/InteropWrapperBuilder.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/api/InteropWrapperBuilder.java @@ -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) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java index c8525e4de33..7751d18aa25 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -784,7 +784,7 @@ public void applyOverdueChargesForLoan(final Long loanId, Collection + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0161_add_loan_external_id_to_commands.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0161_add_loan_external_id_to_commands.xml new file mode 100644 index 00000000000..bc38212121e --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0161_add_loan_external_id_to_commands.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/commands/provider/CommandHandlerProviderStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/commands/provider/CommandHandlerProviderStepDefinitions.java index a422ef19168..8a81e104f5a 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/commands/provider/CommandHandlerProviderStepDefinitions.java +++ b/fineract-provider/src/test/java/org/apache/fineract/commands/provider/CommandHandlerProviderStepDefinitions.java @@ -41,8 +41,8 @@ public CommandHandlerProviderStepDefinitions() { }); 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) -> { diff --git a/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandServiceStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandServiceStepDefinitions.java index c4d694e9da4..9e494ff6644 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandServiceStepDefinitions.java +++ b/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandServiceStepDefinitions.java @@ -101,7 +101,7 @@ public static class DummyCommand extends CommandWrapper { 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 CommandProcessingResult logCommandSource(CommandWrapper wrapper) { 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); } diff --git a/fineract-provider/src/test/java/org/apache/fineract/commands/service/IdempotencyKeyResolverTest.java b/fineract-provider/src/test/java/org/apache/fineract/commands/service/IdempotencyKeyResolverTest.java index 6e6b0af851c..2e3af19d96c 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/commands/service/IdempotencyKeyResolverTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/commands/service/IdempotencyKeyResolverTest.java @@ -77,7 +77,7 @@ public void testIPKResolveFromGenerate() { 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); } diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/creditbureau/service/ThitsaWorksCreditBureauIntegrationWritePlatformServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/creditbureau/service/ThitsaWorksCreditBureauIntegrationWritePlatformServiceImplTest.java index 26e545af628..b83c3729d8a 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/creditbureau/service/ThitsaWorksCreditBureauIntegrationWritePlatformServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/creditbureau/service/ThitsaWorksCreditBureauIntegrationWritePlatformServiceImplTest.java @@ -421,7 +421,7 @@ private JsonCommand initialJsonCommand() throws JsonProcessingException { 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() { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestPauseApiTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestPauseApiTest.java new file mode 100644 index 00000000000..52d52c456ea --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestPauseApiTest.java @@ -0,0 +1,104 @@ +/** + * 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 lombok.extern.slf4j.Slf4j; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.loans.InterestPauseHelper; +import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; +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; + +@Slf4j +@ExtendWith({ LoanTestLifecycleExtension.class }) +public class LoanInterestPauseApiTest extends BaseLoanIntegrationTest { + + private static RequestSpecification REQUEST_SPEC; + private static ResponseSpecification RESPONSE_SPEC; + private static ResponseSpecification RESPONSE_SPEC_403; + private static InterestPauseHelper INTEREST_PAUSE_HELPER; + + @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(); + + INTEREST_PAUSE_HELPER = new InterestPauseHelper(REQUEST_SPEC, RESPONSE_SPEC); + } + + @Test + public void testCreateInterestPauseByLoanId_validRequest_shouldSucceed() { + Long loanId = 1L; + + String response = INTEREST_PAUSE_HELPER.createInterestPauseByLoanId(loanId, "2024-12-01", "2024-12-05", "yyyy-MM-dd", "en"); + + Assertions.assertNotNull(response); + Assertions.assertTrue(response.contains("resourceId")); + } + + @Test + public void testCreateInterestPauseByLoanId_endDateBeforeStartDate_shouldFail() { + Long loanId = 1L; + + String response = INTEREST_PAUSE_HELPER.createInterestPauseByLoanId(loanId, "2024-12-05", "2024-12-01", "yyyy-MM-dd", "en", + RESPONSE_SPEC_403); + + Assertions.assertTrue(response.contains("interest.pause.end.date.before.start.date")); + } + + @Test + public void testCreateInterestPauseByLoanId_startDateBeforeLoanStart_shouldFail() { + Long loanId = 1L; + + String response = INTEREST_PAUSE_HELPER.createInterestPauseByLoanId(loanId, "2022-12-01", "2024-12-05", "yyyy-MM-dd", "en", + RESPONSE_SPEC_403); + + Assertions.assertTrue(response.contains("interest.pause.start.date.before.loan.start.date")); + } + + @Test + public void testCreateInterestPauseByLoanId_endDateAfterLoanMaturity_shouldFail() { + Long loanId = 1L; + + String response = INTEREST_PAUSE_HELPER.createInterestPauseByLoanId(loanId, "2024-12-01", "2025-12-05", "yyyy-MM-dd", "en", + RESPONSE_SPEC_403); + + Assertions.assertTrue(response.contains("interest.pause.end.date.after.loan.maturity.date")); + } + + @Test + public void testRetrieveInterestPausesByLoanId_shouldReturnData() { + Long loanId = 1L; + + String response = INTEREST_PAUSE_HELPER.retrieveInterestPausesByLoanId(loanId); + + Assertions.assertNotNull(response); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/InterestPauseHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/InterestPauseHelper.java new file mode 100644 index 00000000000..0ae390f2906 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/InterestPauseHelper.java @@ -0,0 +1,68 @@ +/** + * 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.common.loans; + +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import org.apache.fineract.integrationtests.client.IntegrationTest; +import org.apache.fineract.integrationtests.common.Utils; + +public class InterestPauseHelper extends IntegrationTest { + + private static final String BASE_URL = "/fineract-provider/api/v1/loans/"; + private final RequestSpecification requestSpec; + private final ResponseSpecification defaultResponseSpec; + + public InterestPauseHelper(final RequestSpecification requestSpec, final ResponseSpecification defaultResponseSpec) { + this.requestSpec = requestSpec; + this.defaultResponseSpec = defaultResponseSpec; + } + + public String createInterestPauseByLoanId(Long loanId, String startDate, String endDate, String dateFormat, String locale) { + return createInterestPauseByLoanId(loanId, startDate, endDate, dateFormat, locale, defaultResponseSpec); + } + + public String createInterestPauseByLoanId(Long loanId, String startDate, String endDate, String dateFormat, String locale, + ResponseSpecification customResponseSpec) { + final String url = BASE_URL + loanId + "/interest-pauses?" + Utils.TENANT_IDENTIFIER; + final String payload = createRequestPayload(startDate, endDate, dateFormat, locale); + return Utils.performServerPost(requestSpec, customResponseSpec, url, payload, null); + } + + public String retrieveInterestPausesByLoanId(Long loanId) { + final String url = BASE_URL + loanId + "/interest-pauses?" + Utils.TENANT_IDENTIFIER; + return Utils.performServerGet(requestSpec, defaultResponseSpec, url); + } + + public String retrieveInterestPausesByExternalId(String externalId) { + final String url = BASE_URL + "external-id/" + externalId + "/interest-pauses?" + Utils.TENANT_IDENTIFIER; + return Utils.performServerGet(requestSpec, defaultResponseSpec, url); + } + + private String createRequestPayload(String startDate, String endDate, String dateFormat, String locale) { + return String.format(""" + { + "startDate": "%s", + "endDate": "%s", + "dateFormat": "%s", + "locale": "%s" + } + """, startDate, endDate, dateFormat, locale); + } +}