This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new c2ec015e6 FINERACT-2152: API Create and retrieve interest pause
c2ec015e6 is described below
commit c2ec015e64876858e12f2e142522c78cb7252e8a
Author: Andrii Kulminskyi <[email protected]>
AuthorDate: Thu Dec 12 14:41:20 2024 +0200
FINERACT-2152: API Create and retrieve interest pause
---
.../fineract/commands/domain/CommandSource.java | 5 +
.../fineract/commands/domain/CommandWrapper.java | 23 +-
.../commands/service/CommandWrapperBuilder.java | 22 +-
...folioCommandSourceWritePlatformServiceImpl.java | 8 +-
.../SynchronousCommandProcessingService.java | 2 +
.../infrastructure/core/api/JsonCommand.java | 31 +-
.../core/data/CommandProcessingResult.java | 12 +-
.../core/data/CommandProcessingResultBuilder.java | 9 +-
...empotencyCommandProcessFailedExceptionTest.java | 2 +-
.../api/LoanInterestPauseApiResource.java | 117 ++++++++
.../data/InterestPauseRequestDto.java | 53 ++++
.../data/InterestPauseResponseDto.java | 45 +++
.../handler/InterestPauseCommandHandler.java | 58 ++++
.../service/InterestPauseReadPlatformService.java | 41 +++
.../InterestPauseReadPlatformServiceImpl.java | 60 ++++
.../service/InterestPauseWritePlatformService.java | 41 +++
.../InterestPauseWritePlatformServiceImpl.java | 136 +++++++++
.../loanaccount/domain/LoanRepository.java | 5 +
.../loanaccount/domain/LoanRepositoryWrapper.java | 14 +
.../loanaccount/domain/LoanTermVariationType.java | 6 +-
.../domain/LoanTermVariationsRepository.java | 11 +
.../AdvancedPaymentAllocationsJsonParserTest.java | 2 +-
.../domain/CreditAllocationsJsonParserTest.java | 2 +-
.../importhandler/center/CenterImportHandler.java | 2 +-
.../importhandler/group/GroupImportHandler.java | 2 +-
...tBureauIntegrationWritePlatformServiceImpl.java | 3 +-
.../interoperation/api/InteropWrapperBuilder.java | 2 +-
.../LoanChargeWritePlatformServiceImpl.java | 2 +-
.../starter/LoanAccountConfiguration.java | 18 ++
.../service/ShareAccountCommandsServiceImpl.java | 2 +-
.../service/ShareProductCommandsServiceImpl.java | 2 +-
.../db/changelog/tenant/changelog-tenant.xml | 1 +
.../0161_add_loan_external_id_to_commands.xml | 33 +++
.../CommandHandlerProviderStepDefinitions.java | 4 +-
.../service/CommandServiceStepDefinitions.java | 4 +-
.../service/IdempotencyKeyResolverTest.java | 2 +-
...eauIntegrationWritePlatformServiceImplTest.java | 2 +-
.../integrationtests/LoanInterestPauseApiTest.java | 315 +++++++++++++++++++++
.../common/loans/LoanTransactionHelper.java | 56 ++++
39 files changed, 1112 insertions(+), 43 deletions(-)
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 97b4cbbe1..ad28f696c 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<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();
}
}
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 135c1bce6..5b51f40de 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 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;
}
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 dad66a861..497862cbf 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 @@ 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;
+ }
}
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 1de67b6e8..dd2d8c81b 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 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);
}
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 3d5917d9d..9b525d5cd 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 @@ 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());
}
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 46f9f2ddb..72ad7c27d 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 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);
}
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 8be8de388..5b07f76bf 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<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) {
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 1536f08be..e630ba707 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 class CommandProcessingResultBuilder {
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 04ba9aa54..d246f833b 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 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);
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 000000000..1b08ad6d8
--- /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<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);
+ }
+}
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 000000000..77efbbcb9
--- /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 000000000..63578cf9d
--- /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 000000000..5747625dd
--- /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 000000000..a55326b3e
--- /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<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);
+}
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 000000000..e8ae87d75
--- /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<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();
+ }
+}
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 000000000..e3b8c397a
--- /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 000000000..272ba3f9f
--- /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<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);
+ }
+ }
+}
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 0ddebb1a6..4db8dedf8 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<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);
}
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 3c0f7526e..bac7cb5b4 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 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));
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 7a462c004..21e4655d3 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 enum LoanTermVariationType {
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 d667825e9..0bf7209ff 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<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);
}
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 85b30ee95..dba4bc6f5 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 @@ 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;
}
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 d33aa1fe8..45138580b 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 @@ 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;
}
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 42878e296..40bfb22d8 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 @@ 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) //
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 2fa48085a..68558fa79 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 @@ 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) //
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 8316d71bd..3a3a2abcf 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 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());
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 20a7c2bc5..6556b50c2 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 c8525e4de..7751d18aa 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 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();
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
index 48d5393f3..b7b9a955d 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
@@ -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);
+ }
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountCommandsServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountCommandsServiceImpl.java
index 24b52b410..708425f92 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountCommandsServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountCommandsServiceImpl.java
@@ -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);
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/service/ShareProductCommandsServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/service/ShareProductCommandsServiceImpl.java
index fcc54c495..b9251e5f1 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/service/ShareProductCommandsServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/service/ShareProductCommandsServiceImpl.java
@@ -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)) {
diff --git
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
index da814c8ad..673a92f1f 100644
---
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
+++
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
@@ -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>
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 000000000..bc3821212
--- /dev/null
+++
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0161_add_loan_external_id_to_commands.xml
@@ -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>
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 a422ef191..8a81e104f 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 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) -> {
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 c4d694e9d..9e494ff66 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 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);
}
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 6e6b0af85..2e3af19d9 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 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);
}
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 26e545af6..b83c3729d 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 @@ 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() {
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 000000000..347b72f23
--- /dev/null
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestPauseApiTest.java
@@ -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)));
+ }
+}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
index c00b13759..174886399 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
@@ -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";
}