From 50790594057b4609e1b395460d0aed4ad0222825 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Thu, 24 Nov 2022 01:50:25 +0100 Subject: [PATCH] FINERACT-1760: Enhanced external id support for loan transactions --- .../commands/domain/CommandSource.java | 47 +- ...CommandSourceWritePlatformServiceImpl.java | 4 +- .../SynchronousCommandProcessingService.java | 5 +- .../domain/ConfigurationDomainService.java | 2 + .../domain/ConfigurationDomainServiceJpa.java | 7 + ...emporaryConfigurationServiceContainer.java | 50 ++ .../core/api/ExternalIdAdapter.java | 42 ++ .../core/data/CommandProcessingResult.java | 27 +- .../data/CommandProcessingResultBuilder.java | 16 +- .../core/domain/ExternalId.java | 84 +++ .../core/domain/ExternalIdConverter.java | 38 ++ .../GoogleGsonSerializerHelper.java | 3 + .../core/service/ExternalIdFactory.java | 60 ++ .../mapper/loan/LoanAccountDataMapper.java | 4 + .../mapper/support/AvroExternalIdMapper.java | 33 ++ .../mapper/support/AvroMapperConfig.java | 2 +- .../jobs/service/JobSchedulerServiceImpl.java | 1 + .../account/data/AccountTransferDTO.java | 9 +- .../domain/AccountTransferDetails.java | 4 + .../ExecuteStandingInstructionsTasklet.java | 5 +- ...ountTransfersWritePlatformServiceImpl.java | 80 +-- .../command/SingleRepaymentCommand.java | 30 +- ...paymentCommandFromApiJsonDeserializer.java | 7 +- ...WritePlatformServiceJpaRepositoryImpl.java | 6 +- .../api/LoanChargesApiResourceSwagger.java | 21 + .../api/LoanTransactionsApiResource.java | 525 ++++++++++++++---- .../LoanTransactionsApiResourceSwagger.java | 81 ++- .../loanaccount/data/LoanTransactionData.java | 18 +- .../portfolio/loanaccount/domain/Loan.java | 80 ++- .../domain/LoanAccountDomainService.java | 25 +- .../domain/LoanAccountDomainServiceJpa.java | 56 +- .../loanaccount/domain/LoanRepository.java | 4 + .../domain/LoanRepositoryWrapper.java | 10 +- .../loanaccount/domain/LoanTransaction.java | 86 +-- .../domain/LoanTransactionRepository.java | 6 + .../exception/LoanNotFoundException.java | 12 +- .../LoanTransactionNotFoundException.java | 14 +- .../service/GuarantorDomainService.java | 2 +- .../service/GuarantorDomainServiceImpl.java | 14 +- .../TransferFeeChargeForLoansTasklet.java | 7 +- .../domain/AbstractLoanScheduleGenerator.java | 2 +- .../LoanChargeApiJsonValidator.java | 8 +- .../LoanEventApiJsonValidator.java | 13 +- .../LoanChargeWritePlatformServiceImpl.java | 111 ++-- .../service/LoanReadPlatformService.java | 4 + .../service/LoanReadPlatformServiceImpl.java | 21 +- ...WritePlatformServiceJpaRepositoryImpl.java | 217 +++++--- .../DepositAccountDomainServiceJpa.java | 21 +- ...DepositAccountReadPlatformServiceImpl.java | 5 +- ...WritePlatformServiceJpaRepositoryImpl.java | 9 +- .../db/changelog/tenant/changelog-tenant.xml | 1 + ...ternal_id_support_for_loan_transaction.xml | 43 ++ .../ClientLoanIntegrationTest.java | 9 +- .../ExternalIdSupportIntegrationTest.java | 431 ++++++++++++++ .../LoanAuditingIntegrationTest.java | 4 +- ...oanTransactionAuditingIntegrationTest.java | 4 +- .../common/GlobalConfigurationHelper.java | 14 +- .../common/loans/LoanTransactionHelper.java | 214 ++++++- 58 files changed, 2140 insertions(+), 518 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/TemporaryConfigurationServiceContainer.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/api/ExternalIdAdapter.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/ExternalId.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/ExternalIdConverter.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/ExternalIdFactory.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/support/AvroExternalIdMapper.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0071_add_external_id_support_for_loan_transaction.xml create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalIdSupportIntegrationTest.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandSource.java b/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandSource.java index f2b34637b..c9a2286d2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandSource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandSource.java @@ -26,7 +26,9 @@ import javax.persistence.ManyToOne; import javax.persistence.Table; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.useradministration.domain.AppUser; @@ -62,7 +64,7 @@ public class CommandSource extends AbstractPersistableCustom { private Long resourceId; @Column(name = "subresource_id") - private Long subresourceId; + private Long subResourceId; @Column(name = "command_as_json", length = 1000) private String commandAsJson; @@ -110,6 +112,12 @@ public class CommandSource extends AbstractPersistableCustom { @Column(name = "idempotency_key", length = 50) private String idempotencyKey; + @Column(name = "resource_external_id") + private ExternalId resourceExternalId; + + @Column(name = "subresource_external_id") + private ExternalId subResourceExternalId; + public static CommandSource fullEntryFrom(final CommandWrapper wrapper, final JsonCommand command, final AppUser maker, String idempotencyKey) { return new CommandSource(wrapper.actionName(), wrapper.entityName(), wrapper.getHref(), command.entityId(), command.subentityId(), @@ -121,12 +129,12 @@ public class CommandSource extends AbstractPersistableCustom { } private CommandSource(final String actionName, final String entityName, final String href, final Long resourceId, - final Long subresourceId, final String commandSerializedAsJson, final AppUser maker, final String idempotencyKey) { + final Long subResourceId, final String commandSerializedAsJson, final AppUser maker, final String idempotencyKey) { this.actionName = actionName; this.entityName = entityName; this.resourceGetUrl = href; this.resourceId = resourceId; - this.subresourceId = subresourceId; + this.subResourceId = subResourceId; this.commandAsJson = commandSerializedAsJson; this.maker = maker; this.madeOnDate = DateUtils.getOffsetDateTimeOfTenant(); @@ -170,8 +178,8 @@ public class CommandSource extends AbstractPersistableCustom { this.resourceId = resourceId; } - public void updateSubresourceId(final Long subresourceId) { - this.subresourceId = subresourceId; + public void updateSubResourceId(final Long subResourceId) { + this.subResourceId = subResourceId; } public void updateJsonTo(final String json) { @@ -182,8 +190,8 @@ public class CommandSource extends AbstractPersistableCustom { return this.resourceId; } - public Long subresourceId() { - return this.subresourceId; + public Long subResourceId() { + return this.subResourceId; } public boolean hasJson() { @@ -210,8 +218,8 @@ public class CommandSource extends AbstractPersistableCustom { return this.resourceId; } - public Long getSubresourceId() { - return this.subresourceId; + public Long getSubResourceId() { + return this.subResourceId; } public void markAsAwaitingApproval() { @@ -226,15 +234,18 @@ public class CommandSource extends AbstractPersistableCustom { return false; } - public void updateForAudit(final Long officeId, final Long groupId, final Long clientId, final Long loanId, final Long savingsId, - final Long productId, final String transactionId) { - this.officeId = officeId; - this.groupId = groupId; - this.clientId = clientId; - this.loanId = loanId; - this.savingsId = savingsId; - this.productId = productId; - this.transactionId = transactionId; + public void updateForAudit(final CommandProcessingResult result) { + this.officeId = result.getOfficeId(); + this.groupId = result.getGroupId(); + this.clientId = result.getClientId(); + this.loanId = result.getLoanId(); + this.savingsId = result.getSavingsId(); + this.productId = result.getProductId(); + this.transactionId = result.getTransactionId(); + this.resourceId = result.getResourceId(); + this.resourceExternalId = result.getResourceExternalId(); + this.subResourceId = result.getSubResourceId(); + this.subResourceExternalId = result.getSubResourceExternalId(); } public String getResourceGetUrl() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java index 188c64c6e..a37861114 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java @@ -82,7 +82,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio validateIsUpdateAllowed(); final CommandWrapper wrapper = CommandWrapper.fromExistingCommand(makerCheckerId, commandSourceInput.getActionName(), - commandSourceInput.getEntityName(), commandSourceInput.resourceId(), commandSourceInput.subresourceId(), + commandSourceInput.getEntityName(), commandSourceInput.resourceId(), commandSourceInput.subResourceId(), commandSourceInput.getResourceGetUrl(), commandSourceInput.getProductId(), commandSourceInput.getOfficeId(), commandSourceInput.getGroupId(), commandSourceInput.getClientId(), commandSourceInput.getLoanId(), commandSourceInput.getSavingsId(), commandSourceInput.getTransactionId(), commandSourceInput.getCreditBureauId(), @@ -90,7 +90,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio final JsonElement parsedCommand = this.fromApiJsonHelper.parse(commandSourceInput.json()); final JsonCommand command = JsonCommand.fromExistingCommand(makerCheckerId, commandSourceInput.json(), parsedCommand, this.fromApiJsonHelper, commandSourceInput.getEntityName(), commandSourceInput.resourceId(), - commandSourceInput.subresourceId(), commandSourceInput.getGroupId(), commandSourceInput.getClientId(), + commandSourceInput.subResourceId(), commandSourceInput.getGroupId(), commandSourceInput.getClientId(), commandSourceInput.getLoanId(), commandSourceInput.getSavingsId(), commandSourceInput.getTransactionId(), commandSourceInput.getResourceGetUrl(), commandSourceInput.getProductId(), commandSourceInput.getCreditBureauId(), commandSourceInput.getOrganisationCreditBureauId(), commandSourceInput.getJobName()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java index a5ea977f3..479eb6d7e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java @@ -110,9 +110,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer ? (requestIdempotencyKey == null ? idempotencyKeyGenerator.create() : requestIdempotencyKey) : wrapper.getIdempotencyKey()); } - commandSourceResult.updateResourceId(result.getResourceId()); - commandSourceResult.updateForAudit(result.getOfficeId(), result.getGroupId(), result.getClientId(), result.getLoanId(), - result.getSavingsId(), result.getProductId(), result.getTransactionId()); + + commandSourceResult.updateForAudit(result); String changesOnlyJson; boolean rollBack = (rollbackTransaction || result.isRollbackTransaction()) && !isApprovedByChecker; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java index 6672d07af..b025e0813 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java @@ -124,4 +124,6 @@ public interface ConfigurationDomainService { Long retrieveRepaymentDueDays(); Long retrieveRepaymentOverdueDays(); + + boolean isExternalIdAutoGenerationEnabled(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index 68c6c539e..be0e45c88 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -43,6 +43,7 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService public static final String EXTERNAL_EVENTS_PURGE_DAYS = "purge-external-events-older-than-days"; private static final String DAYS_BEFORE_REPAYMENT_IS_DUE = "days-before-repayment-is-due"; private static final String DAYS_AFTER_REPAYMENT_IS_OVERDUE = "days-after-repayment-is-overdue"; + private static final String ENABLE_EXTERNAL_ID_AUTO_GENERATION = "enable-auto-generated-external-id"; private final PermissionRepository permissionRepository; private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository; private final PlatformCacheRepository cacheTypeRepository; @@ -473,4 +474,10 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(DAYS_AFTER_REPAYMENT_IS_OVERDUE); return property.getValue(); } + + @Override + public boolean isExternalIdAutoGenerationEnabled() { + final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(ENABLE_EXTERNAL_ID_AUTO_GENERATION); + return property.isEnabled(); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/TemporaryConfigurationServiceContainer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/TemporaryConfigurationServiceContainer.java new file mode 100644 index 000000000..d91c4b174 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/TemporaryConfigurationServiceContainer.java @@ -0,0 +1,50 @@ +/** + * 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.infrastructure.configuration.service; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Component; + +/** + * Temporary (static) configuration service container. + * + * Provide static access to the configuration service TODO: To be deleted when the code cleanup / refactor finished (of + * Loan.java) and we dont need this workaround anymore + */ +@Component +@RequiredArgsConstructor +public class TemporaryConfigurationServiceContainer implements InitializingBean { + + private static volatile ConfigurationDomainService STATIC_REF_CONFIGURATION_SERVICE; + private final ConfigurationDomainService configurationDomainService; + + // To avoid any abuse of this temporary solution, only the `isExternalIdAutoGenerationEnabled()` is exposed + public static boolean isExternalIdAutoGenerationEnabled() { + return TemporaryConfigurationServiceContainer.STATIC_REF_CONFIGURATION_SERVICE.isExternalIdAutoGenerationEnabled(); + } + + @SuppressFBWarnings("ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD") + @Override + public void afterPropertiesSet() throws Exception { + TemporaryConfigurationServiceContainer.STATIC_REF_CONFIGURATION_SERVICE = configurationDomainService; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/api/ExternalIdAdapter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/api/ExternalIdAdapter.java new file mode 100644 index 000000000..bde344d6d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/api/ExternalIdAdapter.java @@ -0,0 +1,42 @@ +/** + * 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.infrastructure.core.api; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; +import org.apache.fineract.infrastructure.core.domain.ExternalId; + +/** + * GSON Serializer for ExternalId + * + */ +public class ExternalIdAdapter implements JsonSerializer { + + @Override + @SuppressWarnings("unused") + public JsonElement serialize(ExternalId src, Type typeOfSrc, JsonSerializationContext context) { + if (src == null || src.isEmpty()) { + return null; + } + return new JsonPrimitive(src.getValue()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java index 385a915b7..70123b19b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.Map; import lombok.Getter; import lombok.ToString; +import org.apache.fineract.infrastructure.core.domain.ExternalId; /** * Represents the successful result of an REST API call that results in processing a command. @@ -47,27 +48,33 @@ public class CommandProcessingResult implements Serializable { private final Long gsimId; private final Long glimId; private Boolean rollbackTransaction; + private final ExternalId resourceExternalId; + private final ExternalId subResourceExternalId; public static CommandProcessingResult fromCommandProcessingResult(CommandProcessingResult commandResult) { return new CommandProcessingResult(commandResult.commandId, commandResult.officeId, commandResult.groupId, commandResult.clientId, commandResult.loanId, commandResult.savingsId, commandResult.resourceIdentifier, commandResult.resourceId, commandResult.transactionId, commandResult.changes, commandResult.productId, commandResult.gsimId, commandResult.glimId, - commandResult.creditBureauReportData, commandResult.rollbackTransaction, commandResult.subResourceId); + commandResult.creditBureauReportData, commandResult.rollbackTransaction, commandResult.subResourceId, + commandResult.resourceExternalId, commandResult.subResourceExternalId); } public static CommandProcessingResult fromCommandProcessingResult(CommandProcessingResult commandResult, final Long resourceId) { return new CommandProcessingResult(commandResult.commandId, commandResult.officeId, commandResult.groupId, commandResult.clientId, commandResult.loanId, commandResult.savingsId, commandResult.resourceIdentifier, resourceId, commandResult.transactionId, commandResult.changes, commandResult.productId, commandResult.gsimId, commandResult.glimId, - commandResult.creditBureauReportData, commandResult.rollbackTransaction, commandResult.subResourceId); + commandResult.creditBureauReportData, commandResult.rollbackTransaction, commandResult.subResourceId, + commandResult.resourceExternalId, commandResult.subResourceExternalId); } public static CommandProcessingResult fromDetails(final Long commandId, final Long officeId, final Long groupId, final Long clientId, final Long loanId, final Long savingsId, final String resourceIdentifier, final Long entityId, final Long gsimId, final Long glimId, final Map creditBureauReportData, final String transactionId, - final Map changes, final Long productId, final Boolean rollbackTransaction, final Long subResourceId) { + final Map changes, final Long productId, final Boolean rollbackTransaction, final Long subResourceId, + final ExternalId resourceExternalId, final ExternalId subResourceExternalId) { return new CommandProcessingResult(commandId, officeId, groupId, clientId, loanId, savingsId, resourceIdentifier, entityId, - transactionId, changes, productId, gsimId, glimId, creditBureauReportData, rollbackTransaction, subResourceId); + transactionId, changes, productId, gsimId, glimId, creditBureauReportData, rollbackTransaction, subResourceId, + resourceExternalId, subResourceExternalId); } public static CommandProcessingResult commandOnlyResult(final Long commandId) { @@ -121,12 +128,15 @@ public class CommandProcessingResult implements Serializable { this.glimId = null; this.creditBureauReportData = null; this.subResourceId = null; + this.resourceExternalId = ExternalId.empty(); + this.subResourceExternalId = ExternalId.empty(); } 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 changesOnly, final Long productId, final Long gsimId, final Long glimId, - final Map creditBureauReportData, Boolean rollbackTransaction, final Long subResourceId) { + final Map creditBureauReportData, Boolean rollbackTransaction, final Long subResourceId, + final ExternalId resourceExternalId, final ExternalId subResourceExternalId) { this.commandId = commandId; this.officeId = officeId; this.groupId = groupId; @@ -143,6 +153,8 @@ public class CommandProcessingResult implements Serializable { this.creditBureauReportData = creditBureauReportData; this.rollbackTransaction = rollbackTransaction; this.subResourceId = subResourceId; + this.resourceExternalId = resourceExternalId; + this.subResourceExternalId = subResourceExternalId; } protected CommandProcessingResult(final Long resourceId, final Long officeId, final Long commandId, @@ -166,11 +178,14 @@ public class CommandProcessingResult implements Serializable { this.glimId = null; this.creditBureauReportData = null; this.subResourceId = null; + this.resourceExternalId = ExternalId.empty(); + this.subResourceExternalId = ExternalId.empty(); } protected CommandProcessingResult(final Long resourceId, final Long officeId, final Long commandId, final Map changesOnly, long clientId) { - this(commandId, officeId, null, clientId, null, null, null, resourceId, null, changesOnly, null, null, null, null, null, null); + this(commandId, officeId, null, clientId, null, null, null, resourceId, null, changesOnly, null, null, null, null, null, null, + ExternalId.empty(), ExternalId.empty()); } public void setOfficeId(final Long officeId) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java index 292081f17..9b8cf9f7e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java @@ -19,6 +19,7 @@ package org.apache.fineract.infrastructure.core.data; import java.util.Map; +import org.apache.fineract.infrastructure.core.domain.ExternalId; /** * Represents the successful result of an REST API call that results in processing a command. @@ -41,11 +42,14 @@ public class CommandProcessingResultBuilder { private Map creditBureauReportData; private Long productId; private boolean rollbackTransaction = false; + private ExternalId entityExternalId = ExternalId.empty(); + + private ExternalId subEntityExternalId = 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.changes, this.productId, this.rollbackTransaction, this.subEntityId, this.entityExternalId, this.subEntityExternalId); } public CommandProcessingResultBuilder withCommandId(final Long withCommandId) { @@ -128,4 +132,14 @@ public class CommandProcessingResultBuilder { return this; } + public CommandProcessingResultBuilder withEntityExternalId(final ExternalId entityExternalId) { + this.entityExternalId = entityExternalId; + return this; + } + + public CommandProcessingResultBuilder withSubEntityExternalId(final ExternalId subEntityExternalId) { + this.subEntityExternalId = subEntityExternalId; + return this; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/ExternalId.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/ExternalId.java new file mode 100644 index 000000000..0b6972fb9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/ExternalId.java @@ -0,0 +1,84 @@ +/** + * 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.infrastructure.core.domain; + +import java.io.Serial; +import java.io.Serializable; +import java.util.UUID; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.exception.PlatformInternalServerException; + +/** + * ExternalId Value object + */ +@Getter +@EqualsAndHashCode +public class ExternalId implements Serializable { + + @Serial + private static final long serialVersionUID = 1; + private static final ExternalId empty = new ExternalId(); + private final String value; + + private ExternalId() { + this.value = null; + } + + public ExternalId(String value) { + if (StringUtils.isBlank(value)) { + throw new IllegalArgumentException("error.external.id.cannot.be.blank"); + } + this.value = value; + } + + /** + * @return Create a new ExternalId object where value is a newly generated UUID + */ + public static ExternalId generate() { + return new ExternalId(UUID.randomUUID().toString()); + } + + /** + * @return Create and return an empty ExternalId object + */ + public static ExternalId empty() { + return empty; + } + + /** + * @return whether value is null for the ExternalId object (return true if value is null) + */ + public boolean isEmpty() { + return value == null; + } + + /** + * Throws exception if value was not set (value is null) for this object + * + * @throws PlatformInternalServerException + * if value was not set (value is null) for this object + */ + public void throwExceptionIfEmpty() { + if (isEmpty()) { + throw new PlatformInternalServerException("error.external.id.is.not.set", "Internal state violation: External id is not set"); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/ExternalIdConverter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/ExternalIdConverter.java new file mode 100644 index 000000000..e67504e77 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/ExternalIdConverter.java @@ -0,0 +1,38 @@ +/** + * 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.infrastructure.core.domain; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; +import org.apache.commons.lang3.StringUtils; + +@Converter(autoApply = true) +public class ExternalIdConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(ExternalId externalId) { + return externalId != null ? externalId.getValue() : null; + } + + @Override + public ExternalId convertToEntityAttribute(String externalId) { + return StringUtils.isBlank(externalId) ? ExternalId.empty() : new ExternalId(externalId); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/GoogleGsonSerializerHelper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/GoogleGsonSerializerHelper.java index 1d561e873..04be13dca 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/GoogleGsonSerializerHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/GoogleGsonSerializerHelper.java @@ -31,6 +31,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import org.apache.fineract.infrastructure.core.api.DateAdapter; +import org.apache.fineract.infrastructure.core.api.ExternalIdAdapter; import org.apache.fineract.infrastructure.core.api.JodaDateTimeAdapter; import org.apache.fineract.infrastructure.core.api.JodaMonthDayAdapter; import org.apache.fineract.infrastructure.core.api.LocalDateAdapter; @@ -39,6 +40,7 @@ import org.apache.fineract.infrastructure.core.api.LocalTimeAdapter; import org.apache.fineract.infrastructure.core.api.OffsetDateTimeAdapter; import org.apache.fineract.infrastructure.core.api.ParameterListExclusionStrategy; import org.apache.fineract.infrastructure.core.api.ParameterListInclusionStrategy; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException; import org.springframework.stereotype.Service; @@ -122,5 +124,6 @@ public final class GoogleGsonSerializerHelper { builder.registerTypeAdapter(MonthDay.class, new JodaMonthDayAdapter()); builder.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter()); builder.registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter()); + builder.registerTypeAdapter(ExternalId.class, new ExternalIdAdapter()); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/ExternalIdFactory.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/ExternalIdFactory.java new file mode 100644 index 000000000..35c8dcd9a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/ExternalIdFactory.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.infrastructure.core.service; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ExternalIdFactory { + + private final ConfigurationDomainService configurationDomainService; + + public static ExternalId produce(String value) { + return StringUtils.isBlank(value) ? ExternalId.empty() : new ExternalId(value); + } + + public ExternalId createFromCommand(JsonCommand command, final String externalIdKey) { + String externalIdStr = null; + if (command.parsedJson() != null) { + externalIdStr = command.stringValueOfParameterNamedAllowingNull(externalIdKey); + } + return create(externalIdStr); + } + + public ExternalId create(String externalIdStr) { + if (StringUtils.isBlank(externalIdStr)) { + if (configurationDomainService.isExternalIdAutoGenerationEnabled()) { + return ExternalId.generate(); + } else { + return ExternalId.empty(); + } + } + return new ExternalId(externalIdStr); + } + + public ExternalId create() { + return create(null); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/loan/LoanAccountDataMapper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/loan/LoanAccountDataMapper.java index 752af6587..50edc033a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/loan/LoanAccountDataMapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/loan/LoanAccountDataMapper.java @@ -19,8 +19,10 @@ package org.apache.fineract.infrastructure.event.external.service.serialization.mapper.loan; import org.apache.fineract.avro.loan.v1.LoanAccountDataV1; +import org.apache.fineract.avro.loan.v1.LoanTransactionDataV1; import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support.AvroMapperConfig; import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData; +import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -33,4 +35,6 @@ public interface LoanAccountDataMapper { @Mapping(source = "topup", target = "isTopup") @Mapping(source = "interestRecalculationEnabled", target = "isInterestRecalculationEnabled") LoanAccountDataV1 map(LoanAccountData source); + + LoanTransactionDataV1 map(LoanTransactionData source); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/support/AvroExternalIdMapper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/support/AvroExternalIdMapper.java new file mode 100644 index 000000000..6900a92d3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/support/AvroExternalIdMapper.java @@ -0,0 +1,33 @@ +/** + * 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.infrastructure.event.external.service.serialization.mapper.support; + +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.springframework.stereotype.Component; + +@Component +public class AvroExternalIdMapper { + + public String mapExternalId(ExternalId source) { + if (source == null) { + return null; + } + return source.getValue(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/support/AvroMapperConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/support/AvroMapperConfig.java index c7ed92f4d..8ee66be99 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/support/AvroMapperConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/support/AvroMapperConfig.java @@ -24,5 +24,5 @@ import org.mapstruct.MappingConstants; import org.mapstruct.ReportingPolicy; @MapperConfig(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.ERROR, builder = @Builder(disableBuilder = true), uses = { - AvroDateTimeMapper.class, AvroMonthDayMapper.class }) + AvroDateTimeMapper.class, AvroMonthDayMapper.class, AvroExternalIdMapper.class }) public class AvroMapperConfig {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobSchedulerServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobSchedulerServiceImpl.java index 54bdcd437..17ec72343 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobSchedulerServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobSchedulerServiceImpl.java @@ -72,6 +72,7 @@ public class JobSchedulerServiceImpl implements ApplicationListener getAccountTransferTransactions() { + return accountTransferTransactions; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsTasklet.java index ec8286247..16fd0c3e1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsTasklet.java @@ -25,6 +25,7 @@ import java.util.Collection; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.AbstractPlatformServiceUnavailableException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -107,8 +108,8 @@ public class ExecuteStandingInstructionsTasklet implements Tasklet { AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount, data.fromAccountType(), data.toAccountType(), data.fromAccount().getId(), data.toAccount().getId(), data.name() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null, - data.transferType().getValue(), null, null, null, null, null, fromSavingsAccount, isRegularTransaction, - isExceptionForBalanceCheck); + data.transferType().getValue(), null, null, ExternalId.empty(), null, null, fromSavingsAccount, + isRegularTransaction, isExceptionForBalanceCheck); final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId()); if (transferCompleted) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java index da811bba6..e32452f58 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java @@ -31,11 +31,14 @@ import java.time.format.DateTimeFormatter; import java.util.Collection; import java.util.List; import java.util.Locale; +import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.portfolio.account.PortfolioAccountType; import org.apache.fineract.portfolio.account.data.AccountTransferDTO; import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator; @@ -63,11 +66,11 @@ import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; import org.apache.fineract.portfolio.savings.domain.SavingsAccountDomainService; import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class AccountTransfersWritePlatformServiceImpl implements AccountTransfersWritePlatformService { private final AccountTransfersDataValidator accountTransfersDataValidator; @@ -82,28 +85,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer private final LoanReadPlatformService loanReadPlatformService; private final GSIMRepositoy gsimRepository; private final ConfigurationDomainService configurationDomainService; - - @Autowired - public AccountTransfersWritePlatformServiceImpl(final AccountTransfersDataValidator accountTransfersDataValidator, - final AccountTransferAssembler accountTransferAssembler, final AccountTransferRepository accountTransferRepository, - final SavingsAccountAssembler savingsAccountAssembler, final SavingsAccountDomainService savingsAccountDomainService, - final LoanAssembler loanAssembler, final LoanAccountDomainService loanAccountDomainService, - final SavingsAccountWritePlatformService savingsAccountWritePlatformService, - final AccountTransferDetailRepository accountTransferDetailRepository, final LoanReadPlatformService loanReadPlatformService, - final GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService) { - this.accountTransfersDataValidator = accountTransfersDataValidator; - this.accountTransferAssembler = accountTransferAssembler; - this.accountTransferRepository = accountTransferRepository; - this.savingsAccountAssembler = savingsAccountAssembler; - this.savingsAccountDomainService = savingsAccountDomainService; - this.loanAccountAssembler = loanAssembler; - this.loanAccountDomainService = loanAccountDomainService; - this.savingsAccountWritePlatformService = savingsAccountWritePlatformService; - this.accountTransferDetailRepository = accountTransferDetailRepository; - this.loanReadPlatformService = loanReadPlatformService; - this.gsimRepository = gsimRepository; - this.configurationDomainService = configurationDomainService; - } + private final ExternalIdFactory externalIdFactory; @Transactional @Override @@ -178,9 +160,11 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer final HolidayDetailDTO holidayDetailDto = null; final boolean isRecoveryRepayment = false; final String chargeRefundChargeType = null; + + ExternalId externalId = externalIdFactory.create(); final LoanTransaction loanRepaymentTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, - toLoanAccount, new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, null, - isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone); + toLoanAccount, transactionDate, transactionAmount, paymentDetail, null, externalId, isRecoveryRepayment, + chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone); final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(command, fromSavingsAccount, toLoanAccount, withdrawal, loanRepaymentTransaction); @@ -193,9 +177,9 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId); - + ExternalId externalId = externalIdFactory.create(); final LoanTransaction loanRefundTransaction = this.loanAccountDomainService.makeRefund(fromLoanAccountId, - new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, null); + new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, externalId); final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName); final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill); @@ -329,12 +313,16 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), transactionBooleanValues, backdatedTxnsAllowedTill); - LoanTransaction loanTransaction = null; + LoanTransaction loanTransaction; + + ExternalId txnExternalId = accountTransferDTO.getTxnExternalId(); + // Safety net (it might need to generate new one) + ExternalId externalId = externalIdFactory.create(txnExternalId.getValue()); if (AccountTransferType.fromInt(accountTransferDTO.getTransferType()).isChargePayment()) { loanTransaction = this.loanAccountDomainService.makeChargePayment(toLoanAccount, accountTransferDTO.getChargeId(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getPaymentDetail(), null, null, accountTransferDTO.getToTransferType(), + accountTransferDTO.getPaymentDetail(), null, externalId, accountTransferDTO.getToTransferType(), accountTransferDTO.getLoanInstallmentNumber()); } else { @@ -343,9 +331,9 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer final HolidayDetailDTO holidayDetailDto = null; final String chargeRefundChargeType = null; loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, toLoanAccount, - new CommandProcessingResultBuilder(), accountTransferDTO.getTransactionDate(), - accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), null, null, isRecoveryRepayment, - chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone); + accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), + accountTransferDTO.getPaymentDetail(), null, externalId, isRecoveryRepayment, chargeRefundChargeType, + isAccountTransfer, holidayDetailDto, isHolidayValidationDone); } accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(accountTransferDTO, fromSavingsAccount, @@ -354,8 +342,8 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer transferTransactionId = accountTransferDetails.getId(); } else if (isSavingsToSavingsAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { - SavingsAccount fromSavingsAccount = null; - SavingsAccount toSavingsAccount = null; + SavingsAccount fromSavingsAccount; + SavingsAccount toSavingsAccount; if (accountTransferDetails == null) { if (accountTransferDTO.getFromSavingsAccount() == null) { fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId(), @@ -421,15 +409,20 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer this.savingsAccountAssembler.setHelpers(toSavingsAccount); } LoanTransaction loanTransaction = null; + + ExternalId txnExternalId = accountTransferDTO.getTxnExternalId(); + // Safety net (it might need to generate new one) + ExternalId externalId = externalIdFactory.create(txnExternalId.getValue()); + if (LoanTransactionType.DISBURSEMENT.getValue().equals(accountTransferDTO.getFromTransferType())) { loanTransaction = this.loanAccountDomainService.makeDisburseTransaction(accountTransferDTO.getFromAccountId(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getPaymentDetail(), accountTransferDTO.getNoteText(), accountTransferDTO.getTxnExternalId()); + accountTransferDTO.getPaymentDetail(), accountTransferDTO.getNoteText(), externalId); } else { loanTransaction = this.loanAccountDomainService.makeRefund(accountTransferDTO.getFromAccountId(), new CommandProcessingResultBuilder(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), accountTransferDTO.getNoteText(), - accountTransferDTO.getTxnExternalId()); + externalId); } final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, @@ -474,13 +467,18 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer this.loanAccountAssembler.setHelpers(toLoanAccount); } + ExternalId externalIdForDisbursement = accountTransferDTO.getTxnExternalId(); + LoanTransaction disburseTransaction = this.loanAccountDomainService.makeDisburseTransaction(accountTransferDTO.getFromAccountId(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), - accountTransferDTO.getNoteText(), accountTransferDTO.getTxnExternalId(), true); + accountTransferDTO.getNoteText(), externalIdForDisbursement, true); final String chargeRefundChargeType = null; + + ExternalId externalIdForRepayment = externalIdFactory.create(); + LoanTransaction repayTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, toLoanAccount, - new CommandProcessingResultBuilder(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getPaymentDetail(), null, null, false, chargeRefundChargeType, isAccountTransfer, null, false, true); + accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), + null, externalIdForRepayment, false, chargeRefundChargeType, isAccountTransfer, null, false, true); AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToLoanTransfer(accountTransferDTO, fromLoanAccount, toLoanAccount, disburseTransaction, repayTransaction); @@ -541,8 +539,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer throw new InvalidPaidInAdvanceAmountException(overpaid.toPlainString()); } + ExternalId externalId = externalIdFactory.create(); + final LoanTransaction loanRefundTransaction = this.loanAccountDomainService.makeRefundForActiveLoan(fromLoanAccountId, - new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, null); + new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, externalId); final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName); final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionsheet/command/SingleRepaymentCommand.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionsheet/command/SingleRepaymentCommand.java index 7ee5dbb55..2d1dc0010 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionsheet/command/SingleRepaymentCommand.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionsheet/command/SingleRepaymentCommand.java @@ -20,39 +20,21 @@ package org.apache.fineract.portfolio.collectionsheet.command; import java.math.BigDecimal; import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; /** * Immutable command for Single loan repayment. */ +@Getter +@AllArgsConstructor public class SingleRepaymentCommand { private final Long loanId; + private final ExternalId externalId; private final BigDecimal transactionAmount; private final LocalDate transactionDate; private final PaymentDetail paymentDetail; - - public SingleRepaymentCommand(final Long loanId, final BigDecimal transactionAmount, final LocalDate transactionDate, - final PaymentDetail paymentDetail) { - this.loanId = loanId; - this.transactionAmount = transactionAmount; - this.transactionDate = transactionDate; - this.paymentDetail = paymentDetail; - } - - public Long getLoanId() { - return this.loanId; - } - - public BigDecimal getTransactionAmount() { - return this.transactionAmount; - } - - public LocalDate getTransactionDate() { - return this.transactionDate; - } - - public PaymentDetail getPaymentDetail() { - return this.paymentDetail; - } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionsheet/serialization/CollectionSheetBulkRepaymentCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionsheet/serialization/CollectionSheetBulkRepaymentCommandFromApiJsonDeserializer.java index d282ca396..1df7450b8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionsheet/serialization/CollectionSheetBulkRepaymentCommandFromApiJsonDeserializer.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionsheet/serialization/CollectionSheetBulkRepaymentCommandFromApiJsonDeserializer.java @@ -25,10 +25,12 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.util.Locale; import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.serialization.AbstractFromApiJsonDeserializer; import org.apache.fineract.infrastructure.core.serialization.FromApiJsonDeserializer; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkRepaymentCommand; import org.apache.fineract.portfolio.collectionsheet.command.SingleRepaymentCommand; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; @@ -90,6 +92,8 @@ public final class CollectionSheetBulkRepaymentCommandFromApiJsonDeserializer final JsonObject loanTransactionElement = array.get(i).getAsJsonObject(); final Long loanId = this.fromApiJsonHelper.extractLongNamed("loanId", loanTransactionElement); + final String externalIdStr = this.fromApiJsonHelper.extractStringNamed("externalId", loanTransactionElement); + final ExternalId externalId = ExternalIdFactory.produce(externalIdStr); final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalNamed("transactionAmount", loanTransactionElement, locale); PaymentDetail detail = paymentDetail; @@ -97,7 +101,8 @@ public final class CollectionSheetBulkRepaymentCommandFromApiJsonDeserializer detail = this.paymentDetailAssembler.fetchPaymentDetail(loanTransactionElement); } if (transactionAmount != null && transactionAmount.intValue() > 0) { - loanRepaymentTransactions[i] = new SingleRepaymentCommand(loanId, transactionAmount, transactionDate, detail); + loanRepaymentTransactions[i] = new SingleRepaymentCommand(loanId, externalId, transactionAmount, transactionDate, + detail); } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionsheet/service/CollectionSheetWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionsheet/service/CollectionSheetWritePlatformServiceJpaRepositoryImpl.java index e3a0f5ef0..d1cfc1ac9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionsheet/service/CollectionSheetWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionsheet/service/CollectionSheetWritePlatformServiceJpaRepositoryImpl.java @@ -91,7 +91,7 @@ public class CollectionSheetWritePlatformServiceJpaRepositoryImpl implements Col } final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - changes.putAll(updateBulkReapayments(command, paymentDetail)); + changes.putAll(updateBulkRepayments(command, paymentDetail)); changes.putAll(updateBulkDisbursals(command)); @@ -122,7 +122,7 @@ public class CollectionSheetWritePlatformServiceJpaRepositoryImpl implements Col final PaymentDetail paymentDetail = null; - changes.putAll(updateBulkReapayments(command, paymentDetail)); + changes.putAll(updateBulkRepayments(command, paymentDetail)); changes.putAll(updateBulkDisbursals(command)); @@ -135,7 +135,7 @@ public class CollectionSheetWritePlatformServiceJpaRepositoryImpl implements Col .with(changes).with(changes).build(); } - private Map updateBulkReapayments(final JsonCommand command, final PaymentDetail paymentDetail) { + private Map updateBulkRepayments(final JsonCommand command, final PaymentDetail paymentDetail) { final Map changes = new HashMap<>(); final CollectionSheetBulkRepaymentCommand bulkRepaymentCommand = this.bulkRepaymentCommandFromApiJsonDeserializer .commandFromApiJson(command.json(), paymentDetail); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResourceSwagger.java index d4eb1ca4f..b01ffb3cb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResourceSwagger.java @@ -270,6 +270,12 @@ final class LoanChargesApiResourceSwagger { public Long savingsId; @Schema(example = "12") public Long resourceId; + @Schema(example = "12") + public Long subResourceId; + @Schema(example = "786444UUUYYH7") + public String resourceExternalId; + @Schema(example = "786444UUUYYH7") + public String subResourceExternalId; public PostLoansLoanIdChargesChargeIdChanges changes; static final class PostLoansLoanIdChargesChargeIdChanges { @@ -288,6 +294,21 @@ final class LoanChargesApiResourceSwagger { public Double amount; @Schema(example = "786444UUUYYH7") public String externalId; + + @Schema(example = "100.00") + public Double principalPortion; + @Schema(example = "100.00") + public Double interestPortion; + @Schema(example = "100.00") + public Double feeChargesPortion; + @Schema(example = "100.00") + public Double penaltyChargesPortion; + @Schema(example = "100.00") + public Double outstandingLoanBalance; + @Schema(example = "19 September 2013") + public Double date; + @Schema(example = "1") + public Long id; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java index a15b976a2..3d65f5697 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java @@ -51,11 +51,13 @@ import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.apache.fineract.infrastructure.core.service.CommandParameterUtil; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; @@ -67,7 +69,7 @@ import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatform import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; -@Path("/loans/{loanId}/transactions") +@Path("/loans") @Component @AllArgsConstructor @Scope("singleton") @@ -88,7 +90,7 @@ public class LoanTransactionsApiResource { private final LoanChargePaidByReadPlatformService loanChargePaidByReadPlatformService; @GET - @Path("template") + @Path("{loanId}/transactions/template") @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Retrieve Loan Transaction Template", description = "This is a convenience resource. It can be useful when building maintenance user interface screens for client applications. The template data returned consists of any or all of:\n" @@ -104,109 +106,115 @@ public class LoanTransactionsApiResource { + "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)") @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class))) }) - public String retrieveTransactionTemplate(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + public String retrieveTransactionTemplate(@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, @QueryParam("command") @Parameter(description = "command") final String commandParam, @Context final UriInfo uriInfo, @QueryParam("dateFormat") @Parameter(description = "dateFormat") final String dateFormat, @QueryParam("transactionDate") @Parameter(description = "transactionDate") final DateParam transactionDateParam, @QueryParam("locale") @Parameter(description = "locale") final String locale) { - this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); - - LoanTransactionData transactionData = null; - if (CommandParameterUtil.is(commandParam, "repayment")) { - transactionData = this.loanReadPlatformService.retrieveLoanTransactionTemplate(loanId); - } else if (CommandParameterUtil.is(commandParam, "merchantIssuedRefund")) { - LocalDate transactionDate = DateUtils.getBusinessLocalDate(); - transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(LoanTransactionType.MERCHANT_ISSUED_REFUND, - loanId, transactionDate); - } else if (CommandParameterUtil.is(commandParam, "payoutRefund")) { - LocalDate transactionDate = DateUtils.getBusinessLocalDate(); - transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(LoanTransactionType.PAYOUT_REFUND, loanId, - transactionDate); - } else if (CommandParameterUtil.is(commandParam, "goodwillCredit")) { - LocalDate transactionDate = DateUtils.getBusinessLocalDate(); - transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(LoanTransactionType.GOODWILL_CREDIT, loanId, - transactionDate); - } else if (CommandParameterUtil.is(commandParam, "waiveinterest")) { - transactionData = this.loanReadPlatformService.retrieveWaiveInterestDetails(loanId); - } else if (CommandParameterUtil.is(commandParam, "writeoff")) { - transactionData = this.loanReadPlatformService.retrieveLoanWriteoffTemplate(loanId); - } else if (CommandParameterUtil.is(commandParam, "close-rescheduled")) { - transactionData = this.loanReadPlatformService.retrieveNewClosureDetails(); - } else if (CommandParameterUtil.is(commandParam, "close")) { - transactionData = this.loanReadPlatformService.retrieveNewClosureDetails(); - } else if (CommandParameterUtil.is(commandParam, "disburse")) { - transactionData = this.loanReadPlatformService.retrieveDisbursalTemplate(loanId, true); - transactionData.setNumberOfRepayments(this.loanReadPlatformService.retrieveNumberOfRepayments(loanId)); - final List loanRepaymentScheduleInstallmentData = this.loanReadPlatformService - .getRepaymentDataResponse(loanId); - transactionData.setLoanRepaymentScheduleInstallments(loanRepaymentScheduleInstallmentData); - } else if (CommandParameterUtil.is(commandParam, "disburseToSavings")) { - transactionData = this.loanReadPlatformService.retrieveDisbursalTemplate(loanId, false); - } else if (CommandParameterUtil.is(commandParam, "recoverypayment")) { - transactionData = this.loanReadPlatformService.retrieveRecoveryPaymentTemplate(loanId); - } else if (CommandParameterUtil.is(commandParam, "prepayLoan")) { - LocalDate transactionDate = null; - if (transactionDateParam == null) { - transactionDate = DateUtils.getBusinessLocalDate(); - } else { - transactionDate = transactionDateParam.getDate("transactionDate", dateFormat, locale); - } - transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(LoanTransactionType.REPAYMENT, loanId, - transactionDate); - } else if (CommandParameterUtil.is(commandParam, "refundbycash")) { - transactionData = this.loanReadPlatformService.retrieveRefundByCashTemplate(loanId); - } else if (CommandParameterUtil.is(commandParam, "refundbytransfer")) { - transactionData = this.loanReadPlatformService.retrieveDisbursalTemplate(loanId, true); - } else if (CommandParameterUtil.is(commandParam, "foreclosure")) { - LocalDate transactionDate = null; - if (transactionDateParam == null) { - transactionDate = DateUtils.getBusinessLocalDate(); - } else { - transactionDate = transactionDateParam.getDate("transactionDate", dateFormat, locale); - } - transactionData = this.loanReadPlatformService.retrieveLoanForeclosureTemplate(loanId, transactionDate); - } else if (CommandParameterUtil.is(commandParam, "creditBalanceRefund")) { - transactionData = this.loanReadPlatformService.retrieveCreditBalanceRefundTemplate(loanId); - } else { - throw new UnrecognizedQueryParamException("command", commandParam); - } - - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.toApiJsonSerializer.serialize(settings, transactionData, this.responseDataParameters); + return retrieveTransactionTemplate(loanId, null, commandParam, uriInfo, dateFormat, transactionDateParam, locale); } @GET - @Path("{transactionId}") + @Path("external-id/{loanExternalId}/transactions/template") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve Loan Transaction Template", description = "This is a convenience resource. It can be useful when building maintenance user interface screens for client applications. The template data returned consists of any or all of:\n" + + "\n" + "Field Defaults\n" + "Allowed Value Lists\n\n" + "Example Requests:\n" + "\n" + + "loans/1/transactions/template?command=repayment" + "loans/1/transactions/template?command=merchantIssuedRefund" + + "loans/1/transactions/template?command=payoutRefund" + "loans/1/transactions/template?command=goodwillCredit" + "\n" + + "loans/1/transactions/template?command=waiveinterest" + "\n" + "loans/1/transactions/template?command=writeoff" + "\n" + + "loans/1/transactions/template?command=close-rescheduled" + "\n" + "loans/1/transactions/template?command=close" + "\n" + + "loans/1/transactions/template?command=disburse" + "\n" + "loans/1/transactions/template?command=disburseToSavings" + "\n" + + "loans/1/transactions/template?command=recoverypayment" + "\n" + "loans/1/transactions/template?command=prepayLoan" + "\n" + + "loans/1/transactions/template?command=refundbycash" + "\n" + "loans/1/transactions/template?command=refundbytransfer" + "\n" + + "loans/1/transactions/template?command=foreclosure" + "\n" + + "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class))) }) + public String retrieveTransactionTemplate( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId, + @QueryParam("command") @Parameter(description = "command") final String commandParam, @Context final UriInfo uriInfo, + @QueryParam("dateFormat") @Parameter(description = "dateFormat") final String dateFormat, + @QueryParam("transactionDate") @Parameter(description = "transactionDate") final DateParam transactionDateParam, + @QueryParam("locale") @Parameter(description = "locale") final String locale) { + + return retrieveTransactionTemplate(null, loanExternalId, commandParam, uriInfo, dateFormat, transactionDateParam, locale); + } + + @GET + @Path("{loanId}/transactions/{transactionId}") @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Retrieve a Transaction Details", description = "Retrieves a Transaction Details\n\n" + "Example Request:\n" + "\n" + "loans/5/transactions/3") @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTransactionIdResponse.class))) }) - public String retrieveTransaction(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, - @PathParam("transactionId") @Parameter(description = "transactionId") final Long transactionId, + public String retrieveTransaction(@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, + @PathParam("transactionId") @Parameter(description = "transactionId", required = true) final Long transactionId, @QueryParam("fields") @Parameter(in = ParameterIn.QUERY, name = "fields", description = "Optional Loan Transaction attribute list to be in the response", required = false, example = "id,date,amount") final String fields, @Context final UriInfo uriInfo) { - this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + return retrieveTransaction(loanId, null, transactionId, null, uriInfo); + } - LoanTransactionData transactionData = this.loanReadPlatformService.retrieveLoanTransaction(loanId, transactionId); - transactionData.setLoanChargePaidByList(this.loanChargePaidByReadPlatformService.getLoanChargesPaidByTransactionId(transactionId)); - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - if (settings.isTemplate()) { - final Collection paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes(); - transactionData = LoanTransactionData.templateOnTop(transactionData, paymentTypeOptions); - } + @GET + @Path("{loanId}/transactions/external-id/{externalTransactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a Transaction Details", description = "Retrieves a Transaction Details\n\n" + "Example Request:\n" + "\n" + + "loans/5/transactions/external-id/5dd80a7c-ccba-4446-b378-01eb6f53e871") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTransactionIdResponse.class))) }) + public String retrieveTransactionByTransactionExternalId( + @PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, + @PathParam("externalTransactionId") @Parameter(description = "externalTransactionId", required = true) final String externalTransactionId, + @QueryParam("fields") @Parameter(in = ParameterIn.QUERY, name = "fields", description = "Optional Loan Transaction attribute list to be in the response", required = false, example = "id,date,amount") final String fields, + @Context final UriInfo uriInfo) { - return this.toApiJsonSerializer.serialize(settings, transactionData, this.responseDataParameters); + return retrieveTransaction(loanId, null, null, externalTransactionId, uriInfo); + } + + @GET + @Path("external-id/{loanExternalId}/transactions/{transactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a Transaction Details", description = "Retrieves a Transaction Details\n\n" + "Example Request:\n" + "\n" + + "loans/5/transactions/3") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTransactionIdResponse.class))) }) + public String retrieveTransactionByLoanExternalIdAndTransactionId( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId, + @PathParam("transactionId") @Parameter(description = "transactionId", required = true) final Long transactionId, + @QueryParam("fields") @Parameter(in = ParameterIn.QUERY, name = "fields", description = "Optional Loan Transaction attribute list to be in the response", required = false, example = "id,date,amount") final String fields, + @Context final UriInfo uriInfo) { + + return retrieveTransaction(null, loanExternalId, transactionId, null, uriInfo); + } + + @GET + @Path("external-id/{loanExternalId}/transactions/external-id/{externalTransactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a Transaction Details", description = "Retrieves a Transaction Details\n\n" + "Example Request:\n" + "\n" + + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions/external-id/5dd80a7c-ccba-4446-b378-01eb6f53e871") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTransactionIdResponse.class))) }) + public String retrieveTransactionByLoanExternalIdAndTransactionExternalId( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId, + @PathParam("externalTransactionId") @Parameter(description = "externalTransactionId", required = true) final String externalTransactionId, + @QueryParam("fields") @Parameter(in = ParameterIn.QUERY, name = "fields", description = "Optional Loan Transaction attribute list to be in the response", required = false, example = "id,date,amount") final String fields, + @Context final UriInfo uriInfo) { + + return retrieveTransaction(null, loanExternalId, null, externalTransactionId, uriInfo); } @POST + @Path("{loanId}/transactions") @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Significant Loan Transactions", description = "This API covers the major loan transaction functionality\n\n" - + "Example Requests:\n" + "\n" + "loans/1/transactions?command=repayment" + " | Make a Repayment | \n" + + "Example Requests:\n\n" + "loans/1/transactions?command=repayment" + " | Make a Repayment | \n" + "loans/1/transactions?command=merchantIssuedRefund" + " | Merchant Issued Refund | \n" + "loans/1/transactions?command=payoutRefund" + " | Payout Refund | \n" + "loans/1/transactions?command=goodwillCredit" + " | Goodwil Credit | \n" + "loans/1/transactions?command=chargeRefund" + " | Charge Refund | \n" @@ -220,41 +228,234 @@ public class LoanTransactionsApiResource { @RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsRequest.class))) @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsResponse.class))) }) - public String executeLoanTransaction(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + public String executeLoanTransaction(@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, @QueryParam("command") @Parameter(description = "command") final String commandParam, @Parameter(hidden = true) final String apiRequestBodyAsJson) { + return executeTransaction(loanId, null, commandParam, apiRequestBodyAsJson); + } + + @POST + @Path("external-id/{loanExternalId}/transactions") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Significant Loan Transactions", description = "This API covers the major loan transaction functionality\n\n" + + "Example Requests:\n\n" + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=repayment" + + " | Make a Repayment | \n" + + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=merchantIssuedRefund" + + " | Merchant Issued Refund | \n" + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=payoutRefund" + + " | Payout Refund | \n" + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=goodwillCredit" + + " | Goodwil Credit | \n" + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=chargeRefund" + + " | Charge Refund | \n" + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=waiveinterest" + + " | Waive Interest | \n" + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=writeoff" + + " | Write-off Loan | \n" + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=close-rescheduled" + + " | Close Rescheduled Loan | \n" + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=close" + + " | Close Loan | \n" + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=undowriteoff" + + " | Undo Loan Write-off | \n" + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=recoverypayment" + + " | Make Recovery Payment | \n" + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=refundByCash" + + " | Make a Refund of an Active Loan by Cash | \n" + + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=foreclosure" + + " | Foreclosure of an Active Loan | \n" + + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=creditBalanceRefund" + " | Credit Balance Refund" + + " | \n") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsResponse.class))) }) + public String executeLoanTransaction( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId, + @QueryParam("command") @Parameter(description = "command") final String commandParam, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + + return executeTransaction(null, loanExternalId, commandParam, apiRequestBodyAsJson); + } + + @POST + @Path("{loanId}/transactions/{transactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Adjust a Transaction", description = "Note: there is no need to specify command={transactionType} parameter.\n\n" + + "Mandatory Fields: transactionDate, transactionAmount") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsTransactionIdRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsResponse.class))) }) + public String adjustLoanTransaction(@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, + @PathParam("transactionId") @Parameter(description = "transactionId", required = true) final Long transactionId, + @Parameter(hidden = true) final String apiRequestBodyAsJson, + @QueryParam("command") @Parameter(description = "command") final String commandParam) { + + return adjustTransaction(loanId, null, transactionId, null, apiRequestBodyAsJson, commandParam); + } + + @POST + @Path("external-id/{loanExternalId}/transactions/{transactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Adjust a Transaction", description = "Note: there is no need to specify command={transactionType} parameter.\n\n" + + "Mandatory Fields: transactionDate, transactionAmount") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsTransactionIdRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsResponse.class))) }) + public String adjustLoanTransaction( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId, + @PathParam("transactionId") @Parameter(description = "transactionId", required = true) final Long transactionId, + @Parameter(hidden = true) final String apiRequestBodyAsJson, + @QueryParam("command") @Parameter(description = "command") final String commandParam) { + + return adjustTransaction(null, loanExternalId, transactionId, null, apiRequestBodyAsJson, commandParam); + } + + @POST + @Path("{loanId}/transactions/external-id/{externalTransactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Adjust a Transaction", description = "Note: there is no need to specify command={transactionType} parameter.\n\n" + + "Mandatory Fields: transactionDate, transactionAmount") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsTransactionIdRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsResponse.class))) }) + public String adjustLoanTransaction(@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, + @PathParam("externalTransactionId") @Parameter(description = "externalTransactionId", required = true) final String transactionExternalId, + @Parameter(hidden = true) final String apiRequestBodyAsJson, + @QueryParam("command") @Parameter(description = "command") final String commandParam) { + + return adjustTransaction(loanId, null, null, transactionExternalId, apiRequestBodyAsJson, commandParam); + } + + @POST + @Path("external-id/{loanExternalId}/transactions/external-id/{externalTransactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Adjust a Transaction", description = "Note: there is no need to specify command={transactionType} parameter.\n\n" + + "Mandatory Fields: transactionDate, transactionAmount") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsTransactionIdRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsResponse.class))) }) + public String adjustLoanTransaction( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId, + @PathParam("externalTransactionId") @Parameter(description = "externalTransactionId", required = true) final String transactionExternalId, + @Parameter(hidden = true) final String apiRequestBodyAsJson, + @QueryParam("command") @Parameter(description = "command") final String commandParam) { + + return adjustTransaction(null, loanExternalId, null, transactionExternalId, apiRequestBodyAsJson, commandParam); + } + + @PUT + @Path("{loanId}/transactions/{transactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Undo a Waive Charge Transaction", description = "Undo a Waive Charge Transaction") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PutChargeTransactionChangesRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PutChargeTransactionChangesResponse.class))) }) + public String undoWaiveCharge(@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, + @PathParam("transactionId") @Parameter(description = "transactionId", required = true) final Long transactionId) { + + return undoWaiveCharge(loanId, null, transactionId, null); + } + + @PUT + @Path("external-id/{loanExternalId}/transactions/{transactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Undo a Waive Charge Transaction", description = "Undo a Waive Charge Transaction") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PutChargeTransactionChangesRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PutChargeTransactionChangesResponse.class))) }) + public String undoWaiveCharge( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId, + @PathParam("transactionId") @Parameter(description = "transactionId", required = true) final Long transactionId) { + + return undoWaiveCharge(null, loanExternalId, transactionId, null); + } + + @PUT + @Path("{loanId}/transactions/external-id/{transactionExternalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Undo a Waive Charge Transaction", description = "Undo a Waive Charge Transaction") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PutChargeTransactionChangesRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PutChargeTransactionChangesResponse.class))) }) + public String undoWaiveCharge(@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, + @PathParam("transactionExternalId") @Parameter(description = "transactionExternalId", required = true) final String transactionExternalId) { + + return undoWaiveCharge(loanId, null, null, transactionExternalId); + } + + @PUT + @Path("external-id/{loanExternalId}/transactions/external-id/{transactionExternalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Undo a Waive Charge Transaction", description = "Undo a Waive Charge Transaction") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PutChargeTransactionChangesRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PutChargeTransactionChangesResponse.class))) }) + public String undoWaiveCharge( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId, + @PathParam("transactionExternalId") @Parameter(description = "transactionExternalId", required = true) final String transactionExternalId) { + + return undoWaiveCharge(null, loanExternalId, null, transactionExternalId); + } + + private String retrieveTransaction(final Long loanId, final String loanExternalIdStr, final Long transactionId, + final String transactionExternalIdStr, final UriInfo uriInfo) { + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + + ExternalId loanExternalId = ExternalIdFactory.produce(loanExternalIdStr); + ExternalId transactionExternalId = ExternalIdFactory.produce(transactionExternalIdStr); + + Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId); + Long resolvedLoanTransactionId = getResolvedLoanTransactionId(transactionId, transactionExternalId); + + LoanTransactionData transactionData = this.loanReadPlatformService.retrieveLoanTransaction(resolvedLoanId, + resolvedLoanTransactionId); + transactionData.setLoanChargePaidByList( + this.loanChargePaidByReadPlatformService.getLoanChargesPaidByTransactionId(transactionData.getId())); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + if (settings.isTemplate()) { + final Collection paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes(); + transactionData = LoanTransactionData.templateOnTop(transactionData, paymentTypeOptions); + } + + return this.toApiJsonSerializer.serialize(settings, transactionData, this.responseDataParameters); + } + + private String executeTransaction(final Long loanId, final String loanExternalIdStr, final String commandParam, + final String apiRequestBodyAsJson) { final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson); + ExternalId loanExternalId = ExternalIdFactory.produce(loanExternalIdStr); + Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId); + CommandWrapper commandRequest = null; if (CommandParameterUtil.is(commandParam, "repayment")) { - commandRequest = builder.loanRepaymentTransaction(loanId).build(); + commandRequest = builder.loanRepaymentTransaction(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "merchantIssuedRefund")) { - commandRequest = builder.loanMerchantIssuedRefundTransaction(loanId).build(); + commandRequest = builder.loanMerchantIssuedRefundTransaction(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "payoutRefund")) { - commandRequest = builder.loanPayoutRefundTransaction(loanId).build(); + commandRequest = builder.loanPayoutRefundTransaction(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "goodwillCredit")) { - commandRequest = builder.loanGoodwillCreditTransaction(loanId).build(); + commandRequest = builder.loanGoodwillCreditTransaction(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "chargeRefund")) { - commandRequest = builder.refundLoanCharge(loanId).build(); + commandRequest = builder.refundLoanCharge(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "waiveinterest")) { - commandRequest = builder.waiveInterestPortionTransaction(loanId).build(); + commandRequest = builder.waiveInterestPortionTransaction(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "writeoff")) { - commandRequest = builder.writeOffLoanTransaction(loanId).build(); + commandRequest = builder.writeOffLoanTransaction(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "close-rescheduled")) { - commandRequest = builder.closeLoanAsRescheduledTransaction(loanId).build(); + commandRequest = builder.closeLoanAsRescheduledTransaction(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "close")) { - commandRequest = builder.closeLoanTransaction(loanId).build(); + commandRequest = builder.closeLoanTransaction(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "undowriteoff")) { - commandRequest = builder.undoWriteOffLoanTransaction(loanId).build(); + commandRequest = builder.undoWriteOffLoanTransaction(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "recoverypayment")) { - commandRequest = builder.loanRecoveryPaymentTransaction(loanId).build(); + commandRequest = builder.loanRecoveryPaymentTransaction(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "refundByCash")) { - commandRequest = builder.refundLoanTransactionByCash(loanId).build(); + commandRequest = builder.refundLoanTransactionByCash(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "foreclosure")) { - commandRequest = builder.loanForeclosure(loanId).build(); + commandRequest = builder.loanForeclosure(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "creditBalanceRefund")) { - commandRequest = builder.creditBalanceRefund(loanId).build(); + commandRequest = builder.creditBalanceRefund(resolvedLoanId).build(); } if (commandRequest == null) { @@ -264,27 +465,92 @@ public class LoanTransactionsApiResource { return this.toApiJsonSerializer.serialize(result); } - @POST - @Path("{transactionId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Adjust a Transaction", description = "Note: there is no need to specify command={transactionType} parameter.\n\n" - + "Mandatory Fields: transactionDate, transactionAmount") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsTransactionIdRequest.class))) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsTransactionIdResponse.class))) }) - public String adjustLoanTransaction(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, - @PathParam("transactionId") @Parameter(description = "transactionId") final Long transactionId, - @Parameter(hidden = true) final String apiRequestBodyAsJson, - @QueryParam("command") @Parameter(description = "command") final String commandParam) { + private String retrieveTransactionTemplate(Long loanId, String loanExternalIdStr, String commandParam, UriInfo uriInfo, + String dateFormat, DateParam transactionDateParam, String locale) { + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + ExternalId loanExternalId = ExternalIdFactory.produce(loanExternalIdStr); + + Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId); + LoanTransactionData transactionData; + + if (CommandParameterUtil.is(commandParam, "repayment")) { + transactionData = this.loanReadPlatformService.retrieveLoanTransactionTemplate(resolvedLoanId); + } else if (CommandParameterUtil.is(commandParam, "merchantIssuedRefund")) { + LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(LoanTransactionType.MERCHANT_ISSUED_REFUND, + resolvedLoanId, transactionDate); + } else if (CommandParameterUtil.is(commandParam, "payoutRefund")) { + LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(LoanTransactionType.PAYOUT_REFUND, resolvedLoanId, + transactionDate); + } else if (CommandParameterUtil.is(commandParam, "goodwillCredit")) { + LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(LoanTransactionType.GOODWILL_CREDIT, + resolvedLoanId, transactionDate); + } else if (CommandParameterUtil.is(commandParam, "waiveinterest")) { + transactionData = this.loanReadPlatformService.retrieveWaiveInterestDetails(resolvedLoanId); + } else if (CommandParameterUtil.is(commandParam, "writeoff")) { + transactionData = this.loanReadPlatformService.retrieveLoanWriteoffTemplate(resolvedLoanId); + } else if (CommandParameterUtil.is(commandParam, "close-rescheduled")) { + transactionData = this.loanReadPlatformService.retrieveNewClosureDetails(); + } else if (CommandParameterUtil.is(commandParam, "close")) { + transactionData = this.loanReadPlatformService.retrieveNewClosureDetails(); + } else if (CommandParameterUtil.is(commandParam, "disburse")) { + transactionData = this.loanReadPlatformService.retrieveDisbursalTemplate(resolvedLoanId, true); + transactionData.setNumberOfRepayments(this.loanReadPlatformService.retrieveNumberOfRepayments(resolvedLoanId)); + final List loanRepaymentScheduleInstallmentData = this.loanReadPlatformService + .getRepaymentDataResponse(resolvedLoanId); + transactionData.setLoanRepaymentScheduleInstallments(loanRepaymentScheduleInstallmentData); + } else if (CommandParameterUtil.is(commandParam, "disburseToSavings")) { + transactionData = this.loanReadPlatformService.retrieveDisbursalTemplate(resolvedLoanId, false); + } else if (CommandParameterUtil.is(commandParam, "recoverypayment")) { + transactionData = this.loanReadPlatformService.retrieveRecoveryPaymentTemplate(resolvedLoanId); + } else if (CommandParameterUtil.is(commandParam, "prepayLoan")) { + LocalDate transactionDate; + if (transactionDateParam == null) { + transactionDate = DateUtils.getBusinessLocalDate(); + } else { + transactionDate = transactionDateParam.getDate("transactionDate", dateFormat, locale); + } + transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(LoanTransactionType.REPAYMENT, resolvedLoanId, + transactionDate); + } else if (CommandParameterUtil.is(commandParam, "refundbycash")) { + transactionData = this.loanReadPlatformService.retrieveRefundByCashTemplate(resolvedLoanId); + } else if (CommandParameterUtil.is(commandParam, "refundbytransfer")) { + transactionData = this.loanReadPlatformService.retrieveDisbursalTemplate(resolvedLoanId, true); + } else if (CommandParameterUtil.is(commandParam, "foreclosure")) { + LocalDate transactionDate; + if (transactionDateParam == null) { + transactionDate = DateUtils.getBusinessLocalDate(); + } else { + transactionDate = transactionDateParam.getDate("transactionDate", dateFormat, locale); + } + transactionData = this.loanReadPlatformService.retrieveLoanForeclosureTemplate(resolvedLoanId, transactionDate); + } else if (CommandParameterUtil.is(commandParam, "creditBalanceRefund")) { + transactionData = this.loanReadPlatformService.retrieveCreditBalanceRefundTemplate(resolvedLoanId); + } else { + throw new UnrecognizedQueryParamException("command", commandParam); + } + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, transactionData, this.responseDataParameters); + } + + private String adjustTransaction(final Long loanId, final String loanExternalIdStr, final Long transactionId, + final String transactionExternalIdStr, final String apiRequestBodyAsJson, final String commandParam) { final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson); - CommandWrapper commandRequest = null; + ExternalId loanExternalId = ExternalIdFactory.produce(loanExternalIdStr); + ExternalId transactionExternalId = ExternalIdFactory.produce(transactionExternalIdStr); + + Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId); + Long resolvedTransactionId = getResolvedLoanTransactionId(transactionId, transactionExternalId); + CommandWrapper commandRequest; if (CommandParameterUtil.is(commandParam, LoanApiConstants.CHARGEBACK_TRANSACTION_COMMAND)) { - commandRequest = builder.chargebackTransaction(loanId, transactionId).build(); + commandRequest = builder.chargebackTransaction(resolvedLoanId, resolvedTransactionId).build(); } else { // Default to adjust the Loan Transaction - commandRequest = builder.adjustTransaction(loanId, transactionId).build(); + commandRequest = builder.adjustTransaction(resolvedLoanId, resolvedTransactionId).build(); } final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); @@ -292,19 +558,34 @@ public class LoanTransactionsApiResource { return this.toApiJsonSerializer.serialize(result); } - @PUT - @Path("{transactionId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Undo a Waive Charge Transaction", description = "Undo a Waive Charge Transaction") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PutChargeTransactionChangesRequest.class))) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PutChargeTransactionChangesResponse.class))) }) - public String undoWaiveCharge(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, - @PathParam("transactionId") @Parameter(description = "transactionId") final Long transactionId) { + private String undoWaiveCharge(final Long loanId, final String loanExternalIdStr, final Long transactionId, + final String transactionExternalIdStr) { + ExternalId loanExternalId = ExternalIdFactory.produce(loanExternalIdStr); + ExternalId transactionExternalId = ExternalIdFactory.produce(transactionExternalIdStr); - final CommandWrapper commandRequest = new CommandWrapperBuilder().undoWaiveChargeTransaction(loanId, transactionId).build(); + Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId); + Long resolvedTransactionId = getResolvedLoanTransactionId(transactionId, transactionExternalId); + final CommandWrapper commandRequest = new CommandWrapperBuilder().undoWaiveChargeTransaction(resolvedLoanId, resolvedTransactionId) + .build(); CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); return this.toApiJsonSerializer.serialize(result); } + + private Long getResolvedLoanTransactionId(final Long transactionId, final ExternalId externalTransactionId) { + Long resolvedLoanTransactionId = transactionId; + if (resolvedLoanTransactionId == null) { + externalTransactionId.throwExceptionIfEmpty(); + resolvedLoanTransactionId = this.loanReadPlatformService.retrieveLoanTransactionIdByExternalId(externalTransactionId); + } + return resolvedLoanTransactionId; + } + + private Long getResolvedLoanId(final Long loanId, final ExternalId loanExternalId) { + Long resolvedLoanId = loanId; + if (resolvedLoanId == null) { + loanExternalId.throwExceptionIfEmpty(); + resolvedLoanId = this.loanReadPlatformService.retrieveLoanIdByExternalId(loanExternalId.getValue()); + } + return resolvedLoanId; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java index 63623fe3a..f488692e4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java @@ -89,6 +89,12 @@ final class LoanTransactionsApiResourceSwagger { @Schema(example = "2") public Long id; + @Schema(example = "3e7791ce-aa10-11ec-b909-0242ac120002") + public String externalId; + @Schema(example = "2") + public Long loanId; + @Schema(example = "3e7791ce-aa10-11ec-b909-0242ac120002") + public String externalLoanId; @Schema(example = "loanTransactionType.repayment") public String code; @Schema(example = "Repayment") @@ -250,6 +256,8 @@ final class LoanTransactionsApiResourceSwagger { public String bankNumber; @Schema(example = "3") public Integer loanChargeId; + @Schema(example = "28 June 2022") + public String dueDate; } @@ -262,8 +270,37 @@ final class LoanTransactionsApiResourceSwagger { public Long officeId; @Schema(example = "1") public Long clientId; + @Schema(example = "1") + public Long loanId; @Schema(example = "22") public Long resourceId; + @Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7") + public String resourceExternalId; + @Schema(example = "22") + public Long subResourceId; + @Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7") + public String subResourceExternalId; + public PostLoansLoanIdTransactionsResponse.Changes changes; + + static final class Changes { + + @Schema(example = "en_GB") + public String locale; + @Schema(example = "dd MMMM yyyy") + public String dateFormat; + @Schema(example = "28 June 2022") + public String transactionDate; + @Schema(example = "50,000.00") + public Double transactionAmount; + @Schema(example = "An optional note about why your adjusting or changing the transaction.") + public String note; + @Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7") + public String reversalExternalId; + @Schema(example = "1") + public Long paymentTypeId; + @Schema(example = "4ff9b1cb988b7") + public String externalId; + } } @Schema(description = "PostLoansLoanIdTransactionsTransactionIdRequest") @@ -289,15 +326,6 @@ final class LoanTransactionsApiResourceSwagger { public String externalId; } - @Schema(description = "PostLoansLoanIdTransactionsTransactionIdResponse") - public static final class PostLoansLoanIdTransactionsTransactionIdResponse { - - private PostLoansLoanIdTransactionsTransactionIdResponse() {} - - @Schema(example = "16") - public Long resourceId; - } - @Schema(description = "PutChargeTransactionChangesResponse") public static final class PutChargeTransactionChangesResponse { @@ -305,16 +333,41 @@ final class LoanTransactionsApiResourceSwagger { static final class Changes { - private Changes() {} + @Schema(example = "10.0") + public Double amount; + @Schema(example = "10.0") + public Double principalPortion; + @Schema(example = "10.0") + public Double interestPortion; + @Schema(example = "10.0") + public Double feeChargesPortion; + @Schema(example = "10.0") + public Double penaltyChargesPortion; + @Schema(example = "10.0") + public Double outstandingLoanBalance; + @Schema(example = "1") + public Long id; + @Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7") + public String externalId; + @Schema(example = "28 June 2022") + public String date; - @Schema(example = "amount") - public String amount; } @Schema(example = "1") - public Long resourceId; - @Schema(example = "48") + public Long officeId; + @Schema(example = "1") + public Long clientId; + @Schema(example = "1") public Long loanId; + @Schema(example = "22") + public Long resourceId; + @Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7") + public String resourceExternalId; + @Schema(example = "22") + public Long subResourceId; + @Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7") + public String subResourceExternalId; public PutChargeTransactionChangesResponse.Changes changes; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java index e527fc6f6..b5d4205be 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java @@ -24,6 +24,7 @@ import java.util.Collection; import java.util.List; import lombok.Getter; import org.apache.fineract.infrastructure.codes.data.CodeValueData; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.account.data.AccountTransferData; import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; @@ -56,7 +57,7 @@ public class LoanTransactionData { private final BigDecimal penaltyChargesPortion; private final BigDecimal overpaymentPortion; private final BigDecimal unrecognizedIncomePortion; - private final String externalId; + private final ExternalId externalId; private final AccountTransferData transfer; private final BigDecimal fixedEmiAmount; private final BigDecimal outstandingLoanBalance; @@ -212,7 +213,7 @@ public class LoanTransactionData { final PaymentDetailData paymentDetailData, final CurrencyData currency, final LocalDate date, final BigDecimal amount, final BigDecimal netDisbursalAmount, final BigDecimal principalPortion, final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, final BigDecimal overpaymentPortion, - final String externalId, final AccountTransferData transfer, BigDecimal fixedEmiAmount, BigDecimal outstandingLoanBalance, + final ExternalId externalId, final AccountTransferData transfer, BigDecimal fixedEmiAmount, BigDecimal outstandingLoanBalance, final BigDecimal unrecognizedIncomePortion, final boolean manuallyReversed, Long loanId, String externalLoanId) { this(id, officeId, officeName, transactionType, paymentDetailData, currency, date, amount, netDisbursalAmount, principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion, overpaymentPortion, unrecognizedIncomePortion, null, externalId, @@ -223,7 +224,7 @@ public class LoanTransactionData { final PaymentDetailData paymentDetailData, final CurrencyData currency, final LocalDate date, final BigDecimal amount, final BigDecimal netDisbursalAmount, final BigDecimal principalPortion, final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, final BigDecimal overpaymentPortion, - BigDecimal unrecognizedIncomePortion, final Collection paymentTypeOptions, final String externalId, + BigDecimal unrecognizedIncomePortion, final Collection paymentTypeOptions, final ExternalId externalId, final AccountTransferData transfer, final BigDecimal fixedEmiAmount, BigDecimal outstandingLoanBalance, boolean manuallyReversed, Long loanId, String externalLoanId) { this(id, externalLoanId, officeId, officeName, transactionType, paymentDetailData, currency, date, amount, netDisbursalAmount, @@ -236,7 +237,7 @@ public class LoanTransactionData { final PaymentDetailData paymentDetailData, final CurrencyData currency, final LocalDate date, final BigDecimal amount, final BigDecimal netDisbursalAmount, final BigDecimal principalPortion, final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, final BigDecimal overpaymentPortion, - final BigDecimal unrecognizedIncomePortion, final String externalId, final AccountTransferData transfer, + final BigDecimal unrecognizedIncomePortion, final ExternalId externalId, final AccountTransferData transfer, BigDecimal fixedEmiAmount, BigDecimal outstandingLoanBalance, LocalDate submittedOnDate, final boolean manuallyReversed, final String reversalExternalId, final LocalDate reversedOnDate, Long loanId, String externalLoanId) { this(id, externalLoanId, officeId, officeName, transactionType, paymentDetailData, currency, date, amount, netDisbursalAmount, @@ -250,7 +251,7 @@ public class LoanTransactionData { final LocalDate date, final BigDecimal amount, final BigDecimal netDisbursalAmount, final BigDecimal principalPortion, final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, final BigDecimal overpaymentPortion, final BigDecimal unrecognizedIncomePortion, - final Collection paymentTypeOptions, final String externalId, final AccountTransferData transfer, + final Collection paymentTypeOptions, final ExternalId externalId, final AccountTransferData transfer, final BigDecimal fixedEmiAmount, BigDecimal outstandingLoanBalance, final LocalDate submittedOnDate, final boolean manuallyReversed, final String reversalExternalId, final LocalDate reversedOnDate, Long loanId) { this.id = id; @@ -308,7 +309,7 @@ public class LoanTransactionData { final BigDecimal feeChargesPortion = null; final BigDecimal penaltyChargesPortion = null; final BigDecimal overpaymentPortion = null; - final String externalId = null; + final ExternalId externalId = ExternalId.empty(); final BigDecimal outstandingLoanBalance = null; final AccountTransferData transfer = null; final LocalDate submittedOnDate = null; @@ -326,8 +327,9 @@ public class LoanTransactionData { BigDecimal netDisbursalAmount, final BigDecimal principalPortion, final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, final BigDecimal overpaymentPortion, BigDecimal unrecognizedIncomePortion, Collection paymentOptions, final AccountTransferData transfer, - final String externalId, final BigDecimal fixedEmiAmount, BigDecimal outstandingLoanBalance, final LocalDate submittedOnDate, - final boolean manuallyReversed, final LocalDate possibleNextRepaymentDate, Long loanId, String externalLoanId) { + final ExternalId externalId, final BigDecimal fixedEmiAmount, BigDecimal outstandingLoanBalance, + final LocalDate submittedOnDate, final boolean manuallyReversed, final LocalDate possibleNextRepaymentDate, Long loanId, + String externalLoanId) { this.id = id; this.loanId = loanId; this.externalLoanId = externalLoanId; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 2e26c00cc..4ca79c749 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -61,9 +61,11 @@ import javax.persistence.UniqueConstraint; import javax.persistence.Version; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.codes.domain.CodeValue; +import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -719,9 +721,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { transactionDate = currentDate; } } - + ExternalId externalId = ExternalId.empty(); + if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } final LoanTransaction applyLoanChargeTransaction = LoanTransaction.accrueLoanCharge(this, getOffice(), chargeAmount, - transactionDate, feeCharges, penaltyCharges); + transactionDate, feeCharges, penaltyCharges, externalId); Integer installmentNumber = null; final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(applyLoanChargeTransaction, loanCharge, @@ -1014,7 +1019,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public LoanTransaction waiveLoanCharge(final LoanCharge loanCharge, final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map changes, final List existingTransactionIds, final List existingReversedTransactionIds, - final Integer loanInstallmentNumber, final ScheduleGeneratorDTO scheduleGeneratorDTO, final Money accruedCharge) { + final Integer loanInstallmentNumber, final ScheduleGeneratorDTO scheduleGeneratorDTO, final Money accruedCharge, + final ExternalId externalId) { validateLoanIsNotClosed(loanCharge); @@ -1066,7 +1072,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); final LoanTransaction waiveLoanChargeTransaction = LoanTransaction.waiveLoanCharge(this, getOffice(), amountWaived, transactionDate, - feeChargesWaived, penaltyChargesWaived, unrecognizedIncome); + feeChargesWaived, penaltyChargesWaived, unrecognizedIncome, externalId); final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(waiveLoanChargeTransaction, loanCharge, waiveLoanChargeTransaction.getAmount(getCurrency()).getAmount(), loanInstallmentNumber); waiveLoanChargeTransaction.getLoanChargesPaid().add(loanChargePaidBy); @@ -1332,12 +1338,18 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private void updateAccrualsForNonPeriodicAccruals(final Collection accruals) { final Money interestApplied = Money.of(getCurrency(), this.summary.getTotalInterestCharged()); + ExternalId externalId = ExternalId.empty(); + boolean isExternalIdAutoGenerationEnabled = TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled(); + for (LoanTransaction loanTransaction : accruals) { if (loanTransaction.getInterestPortion(getCurrency()).isGreaterThanZero()) { if (loanTransaction.getInterestPortion(getCurrency()).isNotEqualTo(interestApplied)) { loanTransaction.reverse(); + if (isExternalIdAutoGenerationEnabled) { + externalId = ExternalId.generate(); + } final LoanTransaction interestAppliedTransaction = LoanTransaction.accrueInterest(getOffice(), this, interestApplied, - getDisbursementDate()); + getDisbursementDate(), externalId); addLoanTransaction(interestAppliedTransaction); } } else { @@ -2457,8 +2469,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { if (((isMultiDisburmentLoan() && getDisbursedLoanDisbursementDetails().size() == 1) || !isMultiDisburmentLoan()) && isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { + ExternalId externalId = ExternalId.empty(); + if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } final LoanTransaction interestAppliedTransaction = LoanTransaction.accrueInterest(getOffice(), this, interestApplied, - actualDisbursementDate); + actualDisbursementDate, externalId); addLoanTransaction(interestAppliedTransaction); } @@ -3415,6 +3431,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private void processIncomeAccrualTransactionOnLoanClosure() { if (this.loanInterestRecalculationDetails != null && this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction() && this.getStatus().isClosedObligationsMet()) { + + ExternalId externalId = ExternalId.empty(); + boolean isExternalIdAutoGenerationEnabled = TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled(); + LocalDate closedDate = this.getClosedOnDate(); reverseTransactionsOnOrAfter(retrieveListOfIncomePostingTransactions(), closedDate); reverseTransactionsOnOrAfter(retrieveListOfAccrualTransactions(), closedDate); @@ -3428,8 +3448,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { BigDecimal penaltyToPost = cumulativeIncomeFromInstallments.get(PENALTY) .subtract(cumulativeIncomeFromIncomePosting.get(PENALTY)); BigDecimal amountToPost = interestToPost.add(feeToPost).add(penaltyToPost); + if (isExternalIdAutoGenerationEnabled) { + externalId = ExternalId.generate(); + } LoanTransaction finalIncomeTransaction = LoanTransaction.incomePosting(this, this.getOffice(), closedDate, amountToPost, - interestToPost, feeToPost, penaltyToPost); + interestToPost, feeToPost, penaltyToPost, externalId); addLoanTransaction(finalIncomeTransaction); if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) { List updatedAccrualTransactions = retrieveListOfAccrualTransactions(); @@ -3439,8 +3462,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } HashMap feeDetails = new HashMap<>(); determineFeeDetails(lastAccruedDate, closedDate, feeDetails); + if (isExternalIdAutoGenerationEnabled) { + externalId = ExternalId.generate(); + } LoanTransaction finalAccrual = LoanTransaction.accrueTransaction(this, this.getOffice(), closedDate, amountToPost, - interestToPost, feeToPost, penaltyToPost); + interestToPost, feeToPost, penaltyToPost, externalId); updateLoanChargesPaidBy(finalAccrual, feeDetails, null); addLoanTransaction(finalAccrual); } @@ -3596,13 +3622,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } return LoanTransaction.waiver(getOffice(), this, possibleInterestToWaive, transactionDate, possibleInterestToWaive, - possibleInterestToWaive.zero()); + possibleInterestToWaive.zero(), ExternalId.empty()); } public ChangedTransactionDetail adjustExistingTransaction(final LoanTransaction newTransactionDetail, final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction transactionForAdjustment, final List existingTransactionIds, final List existingReversedTransactionIds, - final ScheduleGeneratorDTO scheduleGeneratorDTO, final String reversalExternalId) { + final ScheduleGeneratorDTO scheduleGeneratorDTO, final ExternalId reversalExternalId) { HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO(); validateActivityNotBeforeLastTransactionDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, transactionForAdjustment.getTransactionDate()); @@ -3752,11 +3778,18 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final LocalDate writtenOffOnLocalDate = command.localDateValueOfParameterNamed(TRANSACTION_DATE); final String txnExternalId = command.stringValueOfParameterNamedAllowingNull(EXTERNAL_ID); + ExternalId externalId = StringUtils.isBlank(txnExternalId) ? ExternalId.empty() : new ExternalId(txnExternalId); + + if (externalId.isEmpty() && TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } + this.closedOnDate = writtenOffOnLocalDate; this.writtenOffOnDate = writtenOffOnLocalDate; this.closedBy = currentUser; changes.put(CLOSED_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE)); changes.put(WRITTEN_OFF_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE)); + changes.put("externalId", externalId); if (writtenOffOnLocalDate.isBefore(getDisbursementDate())) { final String errorMessage = "The date on which a loan is written off cannot be before the loan disbursement date: " @@ -3772,7 +3805,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.a.future.date", errorMessage, writtenOffOnLocalDate); } - loanTransaction = LoanTransaction.writeoff(this, getOffice(), writtenOffOnLocalDate, txnExternalId); + loanTransaction = LoanTransaction.writeoff(this, getOffice(), writtenOffOnLocalDate, externalId); LocalDate lastTransactionDate = getLastUserTransactionDate(); if (lastTransactionDate.isAfter(writtenOffOnLocalDate)) { final String errorMessage = "The date of the writeoff transaction must occur on or before previous transactions."; @@ -3839,6 +3872,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final LocalDate closureDate = command.localDateValueOfParameterNamed(TRANSACTION_DATE); final String txnExternalId = command.stringValueOfParameterNamedAllowingNull(EXTERNAL_ID); + ExternalId externalId = StringUtils.isBlank(txnExternalId) ? ExternalId.empty() : new ExternalId(txnExternalId); + if (externalId.isEmpty() && TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } + this.closedOnDate = closureDate; changes.put(CLOSED_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE)); @@ -3870,7 +3908,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { changes.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus)); } this.closedOnDate = closureDate; - loanTransaction = LoanTransaction.writeoff(this, getOffice(), closureDate, txnExternalId); + changes.put("externalId", externalId); + loanTransaction = LoanTransaction.writeoff(this, getOffice(), closureDate, externalId); final boolean isLastTransaction = isChronologicallyLatestTransaction(loanTransaction, getLoanTransactions()); if (!isLastTransaction) { final String errorMessage = "The closing date of the loan must be on or after latest transaction date."; @@ -5419,27 +5458,36 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { interest = compoundingDetail.getAmount().subtract(fee).subtract(penalties); } + ExternalId externalId = ExternalId.empty(); + if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } + if (existingIncomeTransaction == null) { LoanTransaction transaction = LoanTransaction.incomePosting(this, this.getOffice(), compoundingDetail.getEffectiveDate(), - compoundingDetail.getAmount(), interest, fee, penalties); + compoundingDetail.getAmount(), interest, fee, penalties, externalId); addLoanTransaction(transaction); } else if (existingIncomeTransaction.getAmount(getCurrency()).getAmount().compareTo(compoundingDetail.getAmount()) != 0) { existingIncomeTransaction.reverse(); LoanTransaction transaction = LoanTransaction.incomePosting(this, this.getOffice(), compoundingDetail.getEffectiveDate(), - compoundingDetail.getAmount(), interest, fee, penalties); + compoundingDetail.getAmount(), interest, fee, penalties, externalId); addLoanTransaction(transaction); } + if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } + if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) { if (existingAccrualTransaction == null) { LoanTransaction accrual = LoanTransaction.accrueTransaction(this, this.getOffice(), compoundingDetail.getEffectiveDate(), - compoundingDetail.getAmount(), interest, fee, penalties); + compoundingDetail.getAmount(), interest, fee, penalties, externalId); updateLoanChargesPaidBy(accrual, feeDetails, null); addLoanTransaction(accrual); } else if (existingAccrualTransaction.getAmount(getCurrency()).getAmount().compareTo(compoundingDetail.getAmount()) != 0) { existingAccrualTransaction.reverse(); LoanTransaction accrual = LoanTransaction.accrueTransaction(this, this.getOffice(), compoundingDetail.getEffectiveDate(), - compoundingDetail.getAmount(), interest, fee, penalties); + compoundingDetail.getAmount(), interest, fee, penalties, externalId); updateLoanChargesPaidBy(accrual, feeDetails, null); addLoanTransaction(accrual); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java index 7c0e52f05..04b473f1c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java @@ -23,32 +23,33 @@ import java.time.LocalDate; import java.util.Map; import java.util.Set; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; public interface LoanAccountDomainService { - LoanTransaction makeRepayment(LoanTransactionType repaymentTransactionType, Loan loan, CommandProcessingResultBuilder builderResult, - LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId, + LoanTransaction makeRepayment(LoanTransactionType repaymentTransactionType, Loan loan, LocalDate transactionDate, + BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, ExternalId txnExternalId, boolean isRecoveryRepayment, String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone); LoanTransaction makeRefund(Long accountId, CommandProcessingResultBuilder builderResult, LocalDate transactionDate, - BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId); + BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, ExternalId txnExternalId); LoanTransaction makeDisburseTransaction(Long loanId, LocalDate transactionDate, BigDecimal transactionAmount, - PaymentDetail paymentDetail, String noteText, String txnExternalId, boolean isLoanToLoanTransfer); + PaymentDetail paymentDetail, String noteText, ExternalId txnExternalId, boolean isLoanToLoanTransfer); void reverseTransfer(LoanTransaction loanTransaction); LoanTransaction makeChargePayment(Loan loan, Long chargeId, LocalDate transactionDate, BigDecimal transactionAmount, - PaymentDetail paymentDetail, String noteText, String txnExternalId, Integer transactionType, Integer installmentNumber); + PaymentDetail paymentDetail, String noteText, ExternalId txnExternalId, Integer transactionType, Integer installmentNumber); LoanTransaction makeDisburseTransaction(Long loanId, LocalDate transactionDate, BigDecimal transactionAmount, - PaymentDetail paymentDetail, String noteText, String txnExternalId); + PaymentDetail paymentDetail, String noteText, ExternalId txnExternalId); LoanTransaction makeRefundForActiveLoan(Long accountId, CommandProcessingResultBuilder builderResult, LocalDate transactionDate, - BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId); + BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, ExternalId txnExternalId); void updateLoanCollateralTransaction(Set loanCollateralManagementList); @@ -71,8 +72,8 @@ public interface LoanAccountDomainService { */ void setLoanDelinquencyTag(Loan loan, LocalDate transactionDate); - LoanTransaction makeRepayment(LoanTransactionType repaymentTransactionType, Loan loan, CommandProcessingResultBuilder builderResult, - LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId, + LoanTransaction makeRepayment(LoanTransactionType repaymentTransactionType, Loan loan, LocalDate transactionDate, + BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, ExternalId txnExternalId, boolean isRecoveryRepayment, String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone, boolean isLoanToLoanTransfer); @@ -82,7 +83,7 @@ public interface LoanAccountDomainService { void saveLoanWithDataIntegrityViolationChecks(Loan loan); - Map foreCloseLoan(Loan loan, LocalDate foreClourseDate, String noteText); + Map foreCloseLoan(Loan loan, LocalDate foreClourseDate, String noteText, ExternalId externalId); /** * Disables all standing instructions linked to a closed loan @@ -94,6 +95,6 @@ public interface LoanAccountDomainService { void recalculateAccruals(Loan loan, boolean isInterestCalcualtionHappened); - CommandProcessingResultBuilder creditBalanceRefund(Long loanId, LocalDate transactionDate, BigDecimal transactionAmount, - String noteText, String externalId); + LoanTransaction creditBalanceRefund(Loan loan, LocalDate transactionDate, BigDecimal transactionAmount, String noteText, + ExternalId externalId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index 996bd00a4..6652bcb1f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -34,10 +34,12 @@ import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDoma import org.apache.fineract.infrastructure.core.data.ApiParameterError; 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.MultiException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargePaymentPostBusinessEvent; @@ -124,17 +126,16 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { private final LoanCollateralManagementRepository loanCollateralManagementRepository; private final DelinquencyWritePlatformService delinquencyWritePlatformService; private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; + private final ExternalIdFactory externalIdFactory; @Transactional @Override public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransactionType, final Loan loan, - final CommandProcessingResultBuilder builderResult, final LocalDate transactionDate, final BigDecimal transactionAmount, - final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, final boolean isRecoveryRepayment, - final String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, - Boolean isHolidayValidationDone) { - return makeRepayment(repaymentTransactionType, loan, builderResult, transactionDate, transactionAmount, paymentDetail, noteText, - txnExternalId, isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone, - false); + final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final String noteText, + final ExternalId txnExternalId, final boolean isRecoveryRepayment, final String chargeRefundChargeType, + boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone) { + return makeRepayment(repaymentTransactionType, loan, transactionDate, transactionAmount, paymentDetail, noteText, txnExternalId, + isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone, false); } @Transactional @@ -155,10 +156,10 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { @Transactional @Override public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransactionType, final Loan loan, - final CommandProcessingResultBuilder builderResult, final LocalDate transactionDate, final BigDecimal transactionAmount, - final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, final boolean isRecoveryRepayment, - final String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, - Boolean isHolidayValidationDone, final boolean isLoanToLoanTransfer) { + final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final String noteText, + final ExternalId txnExternalId, final boolean isRecoveryRepayment, final String chargeRefundChargeType, + boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone, + final boolean isLoanToLoanTransfer) { checkClientOrGroupActive(loan); LoanBusinessEvent repaymentEvent = getLoanRepaymentTypeBusinessEvent(repaymentTransactionType, isRecoveryRepayment, loan); @@ -177,7 +178,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { final List existingReversedTransactionIds = new ArrayList<>(); final Money repaymentAmount = Money.of(loan.getCurrency(), transactionAmount); - LoanTransaction newRepaymentTransaction = null; + LoanTransaction newRepaymentTransaction; if (isRecoveryRepayment) { newRepaymentTransaction = LoanTransaction.recoveryRepayment(loan.getOffice(), repaymentAmount, paymentDetail, transactionDate, txnExternalId); @@ -238,9 +239,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { // changes to closed disableStandingInstructionsLinkedToClosedLoan(loan); - builderResult.withEntityId(newRepaymentTransaction.getId()).withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()) - .withGroupId(loan.getGroupId()); - if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { // Mark Post Dated Check as paid. final Set loanTransactionToRepaymentScheduleMappings = newRepaymentTransaction @@ -358,7 +356,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { @Override @Transactional public LoanTransaction makeChargePayment(final Loan loan, final Long chargeId, final LocalDate transactionDate, - final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, + final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final String noteText, final ExternalId txnExternalId, final Integer transactionType, Integer installmentNumber) { boolean isAccountTransfer = true; checkClientOrGroupActive(loan); @@ -437,7 +435,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { @Override public LoanTransaction makeRefund(final Long accountId, final CommandProcessingResultBuilder builderResult, final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final String noteText, - final String txnExternalId) { + final ExternalId txnExternalId) { boolean isAccountTransfer = true; final Loan loan = this.loanAccountAssembler.assembleFrom(accountId); checkClientOrGroupActive(loan); @@ -477,14 +475,14 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { @Transactional @Override public LoanTransaction makeDisburseTransaction(final Long loanId, final LocalDate transactionDate, final BigDecimal transactionAmount, - final PaymentDetail paymentDetail, final String noteText, final String txnExternalId) { + final PaymentDetail paymentDetail, final String noteText, final ExternalId txnExternalId) { return makeDisburseTransaction(loanId, transactionDate, transactionAmount, paymentDetail, noteText, txnExternalId, false); } @Transactional @Override public LoanTransaction makeDisburseTransaction(final Long loanId, final LocalDate transactionDate, final BigDecimal transactionAmount, - final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, final boolean isLoanToLoanTransfer) { + final PaymentDetail paymentDetail, final String noteText, final ExternalId txnExternalId, final boolean isLoanToLoanTransfer) { final Loan loan = this.loanAccountAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); boolean isAccountTransfer = true; @@ -636,9 +634,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { } @Override - public CommandProcessingResultBuilder creditBalanceRefund(Long loanId, LocalDate transactionDate, BigDecimal transactionAmount, - String noteText, String externalId) { - final Loan loan = this.loanAccountAssembler.assembleFrom(loanId); + public LoanTransaction creditBalanceRefund(final Loan loan, final LocalDate transactionDate, final BigDecimal transactionAmount, + final String noteText, final ExternalId externalId) { businessEventNotifierService.notifyPreBusinessEvent(new LoanCreditBalanceRefundPreBusinessEvent(loan)); final List existingTransactionIds = new ArrayList<>(); final List existingReversedTransactionIds = new ArrayList<>(); @@ -663,15 +660,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { businessEventNotifierService .notifyPostBusinessEvent(new LoanCreditBalanceRefundPostBusinessEvent(newCreditBalanceRefundTransaction)); - return new CommandProcessingResultBuilder().withEntityId(newCreditBalanceRefundTransaction.getId()) // - .withOfficeId(loan.getOfficeId()) // - .withClientId(loan.getClientId()) // - .withGroupId(loan.getGroupId()); + return newCreditBalanceRefundTransaction; } @Override public LoanTransaction makeRefundForActiveLoan(Long accountId, CommandProcessingResultBuilder builderResult, LocalDate transactionDate, - BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId) { + BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, ExternalId txnExternalId) { final Loan loan = this.loanAccountAssembler.assembleFrom(accountId); checkClientOrGroupActive(loan); businessEventNotifierService.notifyPreBusinessEvent(new LoanRefundPreBusinessEvent(loan)); @@ -709,7 +703,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { } @Override - public Map foreCloseLoan(final Loan loan, final LocalDate foreClosureDate, final String noteText) { + public Map foreCloseLoan(final Loan loan, final LocalDate foreClosureDate, final String noteText, + final ExternalId externalId) { businessEventNotifierService.notifyPreBusinessEvent(new LoanForeClosurePreBusinessEvent(loan)); MonetaryCurrency currency = loan.getCurrency(); final Map changes = new LinkedHashMap<>(); @@ -730,8 +725,10 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { Money penaltyPortion = foreCloseDetail.getPenaltyChargesCharged(currency).minus(accruedReceivables[2]); Money total = interestPortion.plus(feePortion).plus(penaltyPortion); if (total.isGreaterThanZero()) { + ExternalId accrualExternalId = externalIdFactory.create(); LoanTransaction accrualTransaction = LoanTransaction.accrueTransaction(loan, loan.getOffice(), foreClosureDate, - total.getAmount(), interestPortion.getAmount(), feePortion.getAmount(), penaltyPortion.getAmount()); + total.getAmount(), interestPortion.getAmount(), feePortion.getAmount(), penaltyPortion.getAmount(), + accrualExternalId); LocalDate fromDate = loan.getDisbursementDate(); if (loan.getAccruedTill() != null) { fromDate = loan.getAccruedTill(); @@ -761,7 +758,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { if (payPrincipal.plus(interestPayable).plus(feePayable).plus(penaltyPayable).isGreaterThanZero()) { final PaymentDetail paymentDetail = null; - String externalId = null; payment = LoanTransaction.repayment(loan.getOffice(), payPrincipal.plus(interestPayable).plus(feePayable).plus(penaltyPayable), paymentDetail, foreClosureDate, externalId); payment.updateLoan(loan); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java index 5e82c1e06..0ddeb8d2b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java @@ -75,6 +75,8 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat String FIND_BY_ACCOUNT_NUMBER = "select loan from Loan loan where loan.accountNumber = :accountNumber"; + String FIND_ID_BY_EXTERNAL_ID = "SELECT loan.id FROM Loan loan WHERE loan.externalId = :externalId"; + @Query(FIND_GROUP_LOANS_DISBURSED_AFTER) List getGroupLoansDisbursedAfter(@Param("disbursementDate") LocalDate disbursementDate, @Param("groupId") Long groupId, @Param("loanType") Integer loanType); @@ -168,4 +170,6 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat @Query(FIND_ALL_NON_CLOSED) List findAllNonClosedLoanIds(); + @Query(FIND_ID_BY_EXTERNAL_ID) + Long findIdByExternalId(@Param("externalId") String externalId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java index 01e78fc50..a33762dac 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java @@ -62,8 +62,7 @@ public class LoanRepositoryWrapper { public Collection findActiveLoansByLoanIdAndGroupId(Long clientId, Long groupId) { final Collection loanStatuses = new ArrayList<>(Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), LoanStatus.APPROVED.getValue(), LoanStatus.ACTIVE.getValue(), LoanStatus.OVERPAID.getValue())); - final Collection loans = this.repository.findByClientIdAndGroupIdAndLoanStatus(clientId, groupId, loanStatuses); - return loans; + return this.repository.findByClientIdAndGroupIdAndLoanStatus(clientId, groupId, loanStatuses); } public Loan saveAndFlush(final Loan loan) { @@ -114,8 +113,7 @@ public class LoanRepositoryWrapper { public List getLoanRepaymentScheduleInstallments(final Long loanId) { final Loan loan = this.repository.findById(loanId).orElseThrow(() -> new LoanNotFoundException(loanId)); - final List loanRepaymentScheduleInstallments = loan.getRepaymentScheduleInstallments(); - return loanRepaymentScheduleInstallments; + return loan.getRepaymentScheduleInstallments(); } public Integer getNumberOfRepayments(final Long loanId) { @@ -253,4 +251,8 @@ public class LoanRepositoryWrapper { } return loan; } + + public Long findIdByExternalId(String externalId) { + return this.repository.findIdByExternalId(externalId); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index 06bf97d11..d01f23725 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -38,6 +38,7 @@ import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; @@ -104,10 +105,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { private boolean reversed; @Column(name = "external_id", length = 100, nullable = true, unique = true) - private String externalId; + private ExternalId externalId; @Column(name = "reversal_external_id", length = 100, nullable = true, unique = true) - private String reversalExternalId; + private ExternalId reversalExternalId; @Column(name = "reversed_on_date", nullable = true) private LocalDate reversedOnDate; @@ -136,30 +137,30 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { protected LoanTransaction() {} public static LoanTransaction incomePosting(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount, - final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion) { + final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, + final ExternalId externalId) { final Integer typeOf = LoanTransactionType.INCOME_POSTING.getValue(); final BigDecimal principalPortion = BigDecimal.ZERO; final BigDecimal overPaymentPortion = BigDecimal.ZERO; final boolean reversed = false; final PaymentDetail paymentDetail = null; - final String externalId = null; return new LoanTransaction(loan, office, typeOf, dateOf, amount, principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion, overPaymentPortion, reversed, paymentDetail, externalId); } public static LoanTransaction disbursement(final Office office, final Money amount, final PaymentDetail paymentDetail, - final LocalDate disbursementDate, final String externalId) { + final LocalDate disbursementDate, final ExternalId externalId) { return new LoanTransaction(null, office, LoanTransactionType.DISBURSEMENT, paymentDetail, amount.getAmount(), disbursementDate, externalId); } public static LoanTransaction repayment(final Office office, final Money amount, final PaymentDetail paymentDetail, - final LocalDate paymentDate, final String externalId) { + final LocalDate paymentDate, final ExternalId externalId) { return new LoanTransaction(null, office, LoanTransactionType.REPAYMENT, paymentDetail, amount.getAmount(), paymentDate, externalId); } public static LoanTransaction chargeback(final Office office, final Money amount, final PaymentDetail paymentDetail, - final LocalDate paymentDate, final String externalId) { + final LocalDate paymentDate, final ExternalId externalId) { LoanTransaction loanTransaction = new LoanTransaction(null, office, LoanTransactionType.CHARGEBACK, paymentDetail, amount.getAmount(), paymentDate, externalId); loanTransaction.principalPortion = amount.getAmount(); @@ -167,13 +168,14 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { } public static LoanTransaction repaymentType(final LoanTransactionType repaymentType, final Office office, final Money amount, - final PaymentDetail paymentDetail, final LocalDate paymentDate, final String externalId, final String chargeRefundChargeType) { + final PaymentDetail paymentDetail, final LocalDate paymentDate, final ExternalId externalId, + final String chargeRefundChargeType) { return new LoanTransaction(null, office, repaymentType, paymentDetail, amount.getAmount(), paymentDate, externalId, chargeRefundChargeType); } public static LoanTransaction chargeAdjustment(final Loan loan, final BigDecimal amount, final LocalDate transactionDate, - final String externalId) { + final ExternalId externalId) { return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.CHARGE_ADJUSTMENT, amount, transactionDate, externalId); } @@ -189,32 +191,32 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { } public static LoanTransaction recoveryRepayment(final Office office, final Money amount, final PaymentDetail paymentDetail, - final LocalDate paymentDate, final String externalId) { + final LocalDate paymentDate, final ExternalId externalId) { return new LoanTransaction(null, office, LoanTransactionType.RECOVERY_REPAYMENT, paymentDetail, amount.getAmount(), paymentDate, externalId); } public static LoanTransaction loanPayment(final Loan loan, final Office office, final Money amount, final PaymentDetail paymentDetail, - final LocalDate paymentDate, final String externalId, final LoanTransactionType transactionType) { + final LocalDate paymentDate, final ExternalId externalId, final LoanTransactionType transactionType) { return new LoanTransaction(loan, office, transactionType, paymentDetail, amount.getAmount(), paymentDate, externalId); } public static LoanTransaction repaymentAtDisbursement(final Office office, final Money amount, final PaymentDetail paymentDetail, - final LocalDate paymentDate, final String externalId) { + final LocalDate paymentDate, final ExternalId externalId) { return new LoanTransaction(null, office, LoanTransactionType.REPAYMENT_AT_DISBURSEMENT, paymentDetail, amount.getAmount(), paymentDate, externalId); } public static LoanTransaction waiver(final Office office, final Loan loan, final Money amount, final LocalDate waiveDate, - final Money waived, final Money unrecognizedPortion) { + final Money waived, final Money unrecognizedPortion, final ExternalId externalId) { LoanTransaction loanTransaction = new LoanTransaction(loan, office, LoanTransactionType.WAIVE_INTEREST, amount.getAmount(), - waiveDate, null); + waiveDate, externalId); loanTransaction.updateInterestComponent(waived, unrecognizedPortion); return loanTransaction; } public static LoanTransaction accrueInterest(final Office office, final Loan loan, final Money amount, - final LocalDate interestAppliedDate) { + final LocalDate interestAppliedDate, final ExternalId externalId) { BigDecimal principalPortion = null; BigDecimal feesPortion = null; BigDecimal penaltiesPortion = null; @@ -222,51 +224,53 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { BigDecimal overPaymentPortion = null; boolean reversed = false; PaymentDetail paymentDetail = null; - String externalId = null; return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), interestAppliedDate, interestPortion, principalPortion, interestPortion, feesPortion, penaltiesPortion, overPaymentPortion, reversed, paymentDetail, externalId); } public static LoanTransaction accrual(final Loan loan, final Office office, final Money amount, final Money interest, - final Money feeCharges, final Money penaltyCharges, final LocalDate transactionDate) { + final Money feeCharges, final Money penaltyCharges, final LocalDate transactionDate, final ExternalId externalId) { return accrueTransaction(loan, office, transactionDate, amount.getAmount(), interest.getAmount(), feeCharges.getAmount(), - penaltyCharges.getAmount()); + penaltyCharges.getAmount(), externalId); } public static LoanTransaction accrueTransaction(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount, - final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion) { + final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, + final ExternalId externalId) { BigDecimal principalPortion = null; BigDecimal overPaymentPortion = null; boolean reversed = false; PaymentDetail paymentDetail = null; - String externalId = null; return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), dateOf, amount, principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion, overPaymentPortion, reversed, paymentDetail, externalId); } - public static LoanTransaction initiateTransfer(final Office office, final Loan loan, final LocalDate transferDate) { + public static LoanTransaction initiateTransfer(final Office office, final Loan loan, final LocalDate transferDate, + final ExternalId externalId) { return new LoanTransaction(loan, office, LoanTransactionType.INITIATE_TRANSFER.getValue(), transferDate, loan.getSummary().getTotalOutstanding(), loan.getSummary().getTotalPrincipalOutstanding(), loan.getSummary().getTotalInterestOutstanding(), loan.getSummary().getTotalFeeChargesOutstanding(), - loan.getSummary().getTotalPenaltyChargesOutstanding(), null, false, null, null); + loan.getSummary().getTotalPenaltyChargesOutstanding(), null, false, null, externalId); } - public static LoanTransaction approveTransfer(final Office office, final Loan loan, final LocalDate transferDate) { + public static LoanTransaction approveTransfer(final Office office, final Loan loan, final LocalDate transferDate, + final ExternalId externalId) { return new LoanTransaction(loan, office, LoanTransactionType.APPROVE_TRANSFER.getValue(), transferDate, loan.getSummary().getTotalOutstanding(), loan.getSummary().getTotalPrincipalOutstanding(), loan.getSummary().getTotalInterestOutstanding(), loan.getSummary().getTotalFeeChargesOutstanding(), - loan.getSummary().getTotalPenaltyChargesOutstanding(), null, false, null, null); + loan.getSummary().getTotalPenaltyChargesOutstanding(), null, false, null, externalId); } - public static LoanTransaction withdrawTransfer(final Office office, final Loan loan, final LocalDate transferDate) { + public static LoanTransaction withdrawTransfer(final Office office, final Loan loan, final LocalDate transferDate, + final ExternalId externalId) { return new LoanTransaction(loan, office, LoanTransactionType.WITHDRAW_TRANSFER.getValue(), transferDate, loan.getSummary().getTotalOutstanding(), loan.getSummary().getTotalPrincipalOutstanding(), loan.getSummary().getTotalInterestOutstanding(), loan.getSummary().getTotalFeeChargesOutstanding(), - loan.getSummary().getTotalPenaltyChargesOutstanding(), null, false, null, null); + loan.getSummary().getTotalPenaltyChargesOutstanding(), null, false, null, externalId); } public static LoanTransaction refund(final Office office, final Money amount, final PaymentDetail paymentDetail, - final LocalDate paymentDate, final String externalId) { + final LocalDate paymentDate, final ExternalId externalId) { return new LoanTransaction(null, office, LoanTransactionType.REFUND, paymentDetail, amount.getAmount(), paymentDate, externalId); } @@ -278,8 +282,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { } public static LoanTransaction accrueLoanCharge(final Loan loan, final Office office, final Money amount, final LocalDate applyDate, - final Money feeCharges, final Money penaltyCharges) { - String externalId = null; + final Money feeCharges, final Money penaltyCharges, final ExternalId externalId) { final LoanTransaction applyCharge = new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL, amount.getAmount(), applyDate, externalId); applyCharge.updateChargesComponents(feeCharges, penaltyCharges); @@ -287,14 +290,14 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { } public static LoanTransaction creditBalanceRefund(final Loan loan, final Office office, final Money amount, final LocalDate paymentDate, - final String externalId) { + final ExternalId externalId) { final PaymentDetail paymentDetail = null; return new LoanTransaction(loan, office, LoanTransactionType.CREDIT_BALANCE_REFUND, paymentDetail, amount.getAmount(), paymentDate, externalId); } public static LoanTransaction refundForActiveLoan(final Office office, final Money amount, final PaymentDetail paymentDetail, - final LocalDate paymentDate, final String externalId) { + final LocalDate paymentDate, final ExternalId externalId) { return new LoanTransaction(null, office, LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, paymentDetail, amount.getAmount(), paymentDate, externalId); } @@ -312,7 +315,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { private LoanTransaction(final Loan loan, final Office office, final Integer typeOf, final LocalDate dateOf, final BigDecimal amount, final BigDecimal principalPortion, final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, final BigDecimal overPaymentPortion, final boolean reversed, - final PaymentDetail paymentDetail, final String externalId) { + final PaymentDetail paymentDetail, final ExternalId externalId) { this.loan = loan; this.typeOf = typeOf; @@ -331,20 +334,21 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { } public static LoanTransaction waiveLoanCharge(final Loan loan, final Office office, final Money waived, final LocalDate waiveDate, - final Money feeChargesWaived, final Money penaltyChargesWaived, final Money unrecognizedCharge) { + final Money feeChargesWaived, final Money penaltyChargesWaived, final Money unrecognizedCharge, final ExternalId externalId) { final LoanTransaction waiver = new LoanTransaction(loan, office, LoanTransactionType.WAIVE_CHARGES, waived.getAmount(), waiveDate, - null); + externalId); waiver.updateChargesComponents(feeChargesWaived, penaltyChargesWaived, unrecognizedCharge); return waiver; } - public static LoanTransaction writeoff(final Loan loan, final Office office, final LocalDate writeOffDate, final String externalId) { + public static LoanTransaction writeoff(final Loan loan, final Office office, final LocalDate writeOffDate, + final ExternalId externalId) { return new LoanTransaction(loan, office, LoanTransactionType.WRITEOFF, null, writeOffDate, externalId); } private LoanTransaction(final Loan loan, final Office office, final LoanTransactionType type, final BigDecimal amount, - final LocalDate date, final String externalId) { + final LocalDate date, final ExternalId externalId) { this.loan = loan; this.typeOf = type.getValue(); this.amount = amount; @@ -355,7 +359,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { } private LoanTransaction(final Loan loan, final Office office, final LoanTransactionType type, final PaymentDetail paymentDetail, - final BigDecimal amount, final LocalDate date, final String externalId) { + final BigDecimal amount, final LocalDate date, final ExternalId externalId) { this.loan = loan; this.typeOf = type.getValue(); this.paymentDetail = paymentDetail; @@ -367,7 +371,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { } private LoanTransaction(final Loan loan, final Office office, final LoanTransactionType type, final PaymentDetail paymentDetail, - final BigDecimal amount, final LocalDate date, final String externalId, final String chargeRefundChargeType) { + final BigDecimal amount, final LocalDate date, final ExternalId externalId, final String chargeRefundChargeType) { this.loan = loan; this.typeOf = type.getValue(); this.paymentDetail = paymentDetail; @@ -386,7 +390,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { this.loanTransactionToRepaymentScheduleMappings.clear(); } - public void reverse(final String reversalExternalId) { + public void reverse(final ExternalId reversalExternalId) { this.reverse(); this.reversalExternalId = reversalExternalId; } @@ -746,7 +750,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { this.loanChargesPaid = loanChargesPaid; } - public String getExternalId() { + public ExternalId getExternalId() { return this.externalId; } @@ -758,7 +762,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { return LoanTransactionType.CREDIT_BALANCE_REFUND.equals(getTypeOf()) && isNotReversed(); } - public void updateExternalId(final String externalId) { + public void updateExternalId(final ExternalId externalId) { this.externalId = externalId; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java index 42429f083..65229fe99 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java @@ -21,13 +21,17 @@ package org.apache.fineract.portfolio.loanaccount.domain; import java.time.LocalDate; import java.util.Collection; import java.util.Optional; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface LoanTransactionRepository extends JpaRepository, JpaSpecificationExecutor { + String FIND_ID_BY_EXTERNAL_ID = "SELECT lt.id FROM LoanTransaction lt WHERE lt.externalId = :externalId"; + Optional findByIdAndLoanId(Long transactionId, Long loanId); @Query(""" @@ -44,4 +48,6 @@ public interface LoanTransactionRepository extends JpaRepository fetchLoanTransactionsByTypeAndLessOrEqualDate(Integer transactionType, LocalDate businessDate); + @Query(FIND_ID_BY_EXTERNAL_ID) + Long findIdByExternalId(@Param("externalId") ExternalId externalId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanNotFoundException.java index 5678c992d..9e8d17a43 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanNotFoundException.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanNotFoundException.java @@ -18,8 +18,8 @@ */ package org.apache.fineract.portfolio.loanaccount.exception; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; -import org.springframework.dao.EmptyResultDataAccessException; /** * A {@link RuntimeException} thrown when loan resources are not found. @@ -30,11 +30,19 @@ public class LoanNotFoundException extends AbstractPlatformResourceNotFoundExcep super("error.msg.loan.id.invalid", "Loan with identifier " + id + " does not exist", id); } - public LoanNotFoundException(Long id, EmptyResultDataAccessException e) { + public LoanNotFoundException(Long id, Exception e) { super("error.msg.loan.id.invalid", "Loan with identifier " + id + " does not exist", id, e); } public LoanNotFoundException(String accountId) { super("error.msg.loan.account.id.invalid", "Loan with account ID " + accountId + " does not exist", accountId); } + + public LoanNotFoundException(ExternalId externalId) { + super("error.msg.loan.external.id.invalid", "Loan with external identifier " + externalId + " does not exist", externalId); + } + + public LoanNotFoundException(ExternalId externalId, Exception e) { + super("error.msg.loan.external.id.invalid", "Loan with external identifier " + externalId + " does not exist", externalId, e); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanTransactionNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanTransactionNotFoundException.java index c0a3b1855..ee31742de 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanTransactionNotFoundException.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanTransactionNotFoundException.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.loanaccount.exception; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; import org.springframework.dao.EmptyResultDataAccessException; @@ -27,15 +28,20 @@ import org.springframework.dao.EmptyResultDataAccessException; public class LoanTransactionNotFoundException extends AbstractPlatformResourceNotFoundException { public LoanTransactionNotFoundException(final Long id) { - super("error.msg.loan.id.invalid", "Transaction with identifier " + id + " does not exist", id); + super("error.msg.loan.transaction.id.invalid", "Transaction with identifier " + id + " does not exist", id); } public LoanTransactionNotFoundException(final Long id, final Long loanId) { - super("error.msg.loan.id.invalid", "Transaction with identifier " + id + " does not exist for loan with identifier " + loanId + ".", - id, loanId); + super("error.msg.loan.transaction.id.invalid", + "Transaction with identifier " + id + " does not exist for loan with identifier " + loanId + ".", id, loanId); } public LoanTransactionNotFoundException(Long id, EmptyResultDataAccessException e) { - super("error.msg.loan.id.invalid", "Transaction with identifier " + id + " does not exist", id, e); + super("error.msg.loan.transaction.id.invalid", "Transaction with identifier " + id + " does not exist", id, e); + } + + public LoanTransactionNotFoundException(ExternalId transactionExternalId, EmptyResultDataAccessException e) { + super("error.msg.loan.transaction.id.invalid", "Transaction with external identifier " + transactionExternalId + " does not exist", + transactionExternalId, e); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorDomainService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorDomainService.java index b3bc00818..fede808c5 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorDomainService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorDomainService.java @@ -30,6 +30,6 @@ public interface GuarantorDomainService { void assignGuarantor(GuarantorFundingDetails guarantorFundingDetails, LocalDate transactionDate); - void transaferFundsFromGuarantor(Loan loan); + void transferFundsFromGuarantor(Loan loan); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorDomainServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorDomainServiceImpl.java index bfdd188dc..dc988e93d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorDomainServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorDomainServiceImpl.java @@ -29,10 +29,13 @@ import java.util.Locale; import java.util.Map; import javax.annotation.PostConstruct; import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.event.business.BusinessEventListener; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanApprovedBusinessEvent; @@ -80,6 +83,8 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService { private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository; private final Map releaseLoanIds = new HashMap<>(2); private final SavingsAccountAssembler savingsAccountAssembler; + private final ConfigurationDomainService configurationDomainService; + private final ExternalIdFactory externalIdFactory; @PostConstruct public void addListeners() { @@ -204,7 +209,7 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService { * account to loan account and releases guarantor) */ @Override - public void transaferFundsFromGuarantor(final Loan loan) { + public void transferFundsFromGuarantor(final Loan loan) { if (loan.getGuaranteeAmount().compareTo(BigDecimal.ZERO) <= 0) { return; } @@ -227,7 +232,6 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService { final AccountTransferDetails accountTransferDetails = null; final String noteText = null; - final String txnExternalId = null; final SavingsAccount toSavingsAccount = null; Long loanId = loan.getId(); @@ -245,11 +249,11 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService { remainingAmount = remainingAmount.multiply(loan.getPrincipal().getAmount()).divide(loan.getGuaranteeAmount(), MoneyHelper.getRoundingMode()); } + ExternalId externalId = externalIdFactory.create(); AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, remainingAmount, fromAccountType, toAccountType, fromAccountId, toAccountId, description, locale, fmt, paymentDetail, fromTransferType, - toTransferType, chargeId, loanInstallmentNumber, transferType, accountTransferDetails, noteText, - txnExternalId, loan, toSavingsAccount, fromSavingsAccount, isRegularTransaction, - isExceptionForBalanceCheck); + toTransferType, chargeId, loanInstallmentNumber, transferType, accountTransferDetails, noteText, externalId, + loan, toSavingsAccount, fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); transferAmount(accountTransferDTO); } finally { releaseLoanIds.remove(loanId); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/transferfeechargeforloans/TransferFeeChargeForLoansTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/transferfeechargeforloans/TransferFeeChargeForLoansTasklet.java index 6451387ac..66c03a401 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/transferfeechargeforloans/TransferFeeChargeForLoansTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/transferfeechargeforloans/TransferFeeChargeForLoansTasklet.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; import org.apache.fineract.portfolio.account.PortfolioAccountType; @@ -74,7 +75,7 @@ public class TransferFeeChargeForLoansTasklet implements Tasklet { portfolioAccountData.getId(), chargeData.getLoanId(), "Loan Charge Payment", null, null, null, null, LoanTransactionType.CHARGE_PAYMENT.getValue(), chargeData.getId(), installmentChargeData.getInstallmentNumber(), AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, - null, null, null, null, isRegularTransaction, isExceptionForBalanceCheck); + ExternalId.empty(), null, null, null, isRegularTransaction, isExceptionForBalanceCheck); transferFeeCharge(accountTransferDTO, errors); } } @@ -86,8 +87,8 @@ public class TransferFeeChargeForLoansTasklet implements Tasklet { chargeData.getAmountOutstanding(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, portfolioAccountData.getId(), chargeData.getLoanId(), "Loan Charge Payment", null, null, null, null, LoanTransactionType.CHARGE_PAYMENT.getValue(), chargeData.getId(), null, - AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null, null, isRegularTransaction, - isExceptionForBalanceCheck); + AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, ExternalId.empty(), null, null, null, + isRegularTransaction, isExceptionForBalanceCheck); transferFeeCharge(accountTransferDTO, errors); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java index 1d1cc4ac3..991e53d0c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java @@ -1933,7 +1933,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener periods.add(disbursementPeriod); principal = principal.add(disbursementData.getPrincipal()); } else if (!excludePastUndisbursed || disbursementData.isDisbursed() - || !disbursementData.disbursementDate().isBefore(DateUtils.getLocalDateOfTenant())) { + || !disbursementData.disbursementDate().isBefore(DateUtils.getBusinessLocalDate())) { /* * JW: sums up amounts by disbursal date in case of side-effect issues. Original assumed that there * were no duplicate disbursal dates and 'put' each amount into the map keyed by date diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeApiJsonValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeApiJsonValidator.java index d4f470997..2de7cb129 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeApiJsonValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeApiJsonValidator.java @@ -119,9 +119,10 @@ public final class LoanChargeApiJsonValidator { Set transactionParameters = null; if (isChargeIdIncluded) { transactionParameters = new HashSet<>( - Arrays.asList("transactionDate", "locale", "dateFormat", "chargeId", "dueDate", "installmentNumber")); + Arrays.asList("transactionDate", "locale", "dateFormat", "chargeId", "dueDate", "installmentNumber", "externalId")); } else { - transactionParameters = new HashSet<>(Arrays.asList("transactionDate", "locale", "dateFormat", "dueDate", "installmentNumber")); + transactionParameters = new HashSet<>( + Arrays.asList("transactionDate", "locale", "dateFormat", "dueDate", "installmentNumber", "externalId")); } final Type typeOfMap = new TypeToken>() { @@ -215,7 +216,8 @@ public final class LoanChargeApiJsonValidator { if (StringUtils.isBlank(json)) { return; } - Set transactionParameters = new HashSet<>(Arrays.asList("dueDate", "locale", "dateFormat", "installmentNumber")); + Set transactionParameters = new HashSet<>( + Arrays.asList("dueDate", "locale", "dateFormat", "installmentNumber", "externalId")); final Type typeOfMap = new TypeToken>() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java index b6912d556..f39d5cdad 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java @@ -320,7 +320,7 @@ public final class LoanEventApiJsonValidator { } final Set disbursementParameters = new HashSet<>( - Arrays.asList("transactionDate", "note", "locale", "dateFormat", "writeoffReasonId")); + Arrays.asList("transactionDate", "note", "locale", "dateFormat", "writeoffReasonId", "externalId")); final Type typeOfMap = new TypeToken>() {}.getType(); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters); @@ -335,6 +335,9 @@ public final class LoanEventApiJsonValidator { final String note = this.fromApiJsonHelper.extractStringNamed("note", element); baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000); + final String externalId = this.fromApiJsonHelper.extractStringNamed("externalId", element); + baseDataValidator.reset().parameter("externalId").value(externalId).ignoreIfNull().notExceedingLengthOf(100); + throwExceptionIfValidationWarningsExist(dataValidationErrors); } @@ -482,7 +485,7 @@ public final class LoanEventApiJsonValidator { baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000); final String externalId = this.fromApiJsonHelper.extractStringNamed("externalId", element); - baseDataValidator.reset().parameter("externalId").value(externalId).notExceedingLengthOf(100); + baseDataValidator.reset().parameter("externalId").value(externalId).ignoreIfNull().notExceedingLengthOf(100); validatePaymentDetails(baseDataValidator, element); throwExceptionIfValidationWarningsExist(dataValidationErrors); @@ -494,7 +497,8 @@ public final class LoanEventApiJsonValidator { throw new InvalidJsonException(); } - final Set foreclosureParameters = new HashSet<>(Arrays.asList("transactionDate", "note", "locale", "dateFormat")); + final Set foreclosureParameters = new HashSet<>( + Arrays.asList("transactionDate", "note", "locale", "dateFormat", "externalId")); final Type typeOfMap = new TypeToken>() {}.getType(); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, foreclosureParameters); @@ -509,6 +513,9 @@ public final class LoanEventApiJsonValidator { final String note = this.fromApiJsonHelper.extractStringNamed("note", element); baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000); + final String externalId = this.fromApiJsonHelper.extractStringNamed("externalId", element); + baseDataValidator.reset().parameter("externalId").value(externalId).ignoreIfNull().notExceedingLengthOf(100); + validatePaymentDetails(baseDataValidator, element); throwExceptionIfValidationWarningsExist(dataValidationErrors); } 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 d1fb1d42b..da86b0967 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 @@ -41,9 +41,11 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; 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.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanApplyOverdueChargeBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanAddChargeBusinessEvent; @@ -60,6 +62,8 @@ import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.account.PortfolioAccountType; import org.apache.fineract.portfolio.account.data.AccountTransferDTO; import org.apache.fineract.portfolio.account.data.PortfolioAccountData; +import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository; +import org.apache.fineract.portfolio.account.domain.AccountTransferDetails; import org.apache.fineract.portfolio.account.domain.AccountTransferType; import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; @@ -81,6 +85,7 @@ import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollatera import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; +import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByData; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; @@ -146,6 +151,9 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo private final FromJsonHelper fromApiJsonHelper; private final ConfigurationDomainService configurationDomainService; private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; + private final ExternalIdFactory externalIdFactory; + + private final AccountTransferDetailRepository accountTransferDetailRepository; private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) { return e.getFromDate().isBefore(loanCharge.getDueDate()) && !loanCharge.getDueDate().isAfter(e.getDueDate()); @@ -242,8 +250,12 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); businessEventNotifierService.notifyPostBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge)); - return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loanCharge.getId()) - .withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()).withGroupId(loan.getGroupId()).withLoanId(loanId) + return new CommandProcessingResultBuilder().withCommandId(command.commandId()) // + .withEntityId(loanCharge.getId()) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(loanId) // .build(); } @@ -342,17 +354,21 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveChargeUndoBusinessEvent(loanCharge)); + // TODO: this needs to be fixed in the future! Principal portion is bigdecimal, interest portion is money, ... changes.put("principalPortion", loanTransaction.getPrincipalPortion()); changes.put("interestPortion", loanTransaction.getInterestPortion(loan.getCurrency())); changes.put("feeChargesPortion", loanTransaction.getFeeChargesPortion(loan.getCurrency())); changes.put("penaltyChargesPortion", loanTransaction.getPenaltyChargesPortion(loan.getCurrency())); changes.put("outstandingLoanBalance", loanTransaction.getOutstandingLoanBalance()); changes.put("id", loanTransaction.getId()); + changes.put("externalId", loanTransaction.getExternalId()); changes.put("date", loanTransaction.getTransactionDate()); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(loanCharge.getId()) // + .withSubEntityId(loanTransaction.getId()) // + .withSubEntityExternalId(loanTransaction.getExternalId()) // .withLoanId(loanId) // .with(changes).build(); } @@ -398,6 +414,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo final Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); this.loanChargeApiJsonValidator.validateInstallmentChargeTransaction(command.json()); + final ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); // Charges may be waived only when the loan associated with them are @@ -442,6 +459,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo } final Map changes = new LinkedHashMap<>(3); + changes.put(LoanApiConstants.externalIdParameterName, externalId); final List existingTransactionIds = new ArrayList<>(); final List existingReversedTransactionIds = new ArrayList<>(); @@ -458,7 +476,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo } final LoanTransaction waiveTransaction = loan.waiveLoanCharge(loanCharge, defaultLoanLifecycleStateMachine, changes, - existingTransactionIds, existingReversedTransactionIds, loanInstallmentNumber, scheduleGeneratorDTO, accruedCharge); + existingTransactionIds, existingReversedTransactionIds, loanInstallmentNumber, scheduleGeneratorDTO, accruedCharge, + externalId); this.loanTransactionRepository.saveAndFlush(waiveTransaction); this.loanRepositoryWrapper.save(loan); @@ -471,6 +490,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(loanChargeId) // + .withSubEntityId(waiveTransaction.getId()) // + .withSubEntityExternalId(waiveTransaction.getExternalId()) // .withOfficeId(loan.getOfficeId()) // .withClientId(loan.getClientId()) // .withGroupId(loan.getGroupId()) // @@ -521,6 +542,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo final Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); + final ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); // Charges may be waived only when the loan associated with them are // active @@ -583,12 +605,15 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, amount, PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, portfolioAccountData.getId(), loanId, "Loan Charge Payment", locale, fmt, null, null, LoanTransactionType.CHARGE_PAYMENT.getValue(), loanChargeId, loanInstallmentNumber, - AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null, fromSavingsAccount, isRegularTransaction, + AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, externalId, null, null, fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); - this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); + Long transferTransactionId = this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); + AccountTransferDetails transferDetails = this.accountTransferDetailRepository.findById(transferTransactionId).get(); + LoanTransaction loanTransaction = transferDetails.getAccountTransferTransactions().get(0).getToLoanTransaction(); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(loanChargeId) // + .withSubEntityId(loanTransaction.getId()).withSubEntityExternalId(loanTransaction.getExternalId()) .withOfficeId(loan.getOfficeId()) // .withClientId(loan.getClientId()) // .withGroupId(loan.getGroupId()) // @@ -604,7 +629,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("amount"); - final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId"); + final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, "externalId"); final String locale = command.locale(); loanChargeAdjustmentEntranceValidation(loanCharge, transactionAmount); @@ -639,44 +664,12 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo changes.put("locale", locale); return commandProcessingResultBuilder.withCommandId(command.commandId()) // .withLoanId(loanId) // - .withEntityId(loanTransaction.getId()).with(changes) // + .withEntityId(loanChargeId).withSubEntityId(loanTransaction.getId()) // + .withSubEntityExternalId(loanTransaction.getExternalId()) // + .with(changes) // .build(); } - private LoanTransaction applyChargeAdjustment(final Loan loan, final LoanCharge loanCharge, final BigDecimal transactionAmount, - final LocalDate transactionDate, final String txnExternalId) { - businessEventNotifierService.notifyPreBusinessEvent(new LoanChargeAdjustmentPreBusinessEvent(loan)); - final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); - final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); - - LoanTransaction loanChargeAdjustmentTransaction = LoanTransaction.chargeAdjustment(loan, transactionAmount, transactionDate, - txnExternalId); - LoanTransactionRelation loanTransactionRelation = LoanTransactionRelation.linkToCharge(loanChargeAdjustmentTransaction, loanCharge, - LoanTransactionRelationTypeEnum.CHARGE_ADJUSTMENT); - loanChargeAdjustmentTransaction.getLoanTransactionRelations().add(loanTransactionRelation); - - loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(loanChargeAdjustmentTransaction); - - defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan); - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory - .determineProcessor(loan.transactionProcessingStrategy()); - loanRepaymentScheduleTransactionProcessor.handleTransaction(loanChargeAdjustmentTransaction, loan.getCurrency(), - loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); - - loan.addLoanTransaction(loanChargeAdjustmentTransaction); - loan.updateLoanSummaryAndStatus(); - - loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan); - - final Map accountingBridgeData = loan.deriveAccountingBridgeData(loan.getCurrency().getCode(), - existingTransactionIds, existingReversedTransactionIds, false); - this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); - - loanAccountDomainService.setLoanDelinquencyTag(loan, transactionDate); - - return loanChargeAdjustmentTransaction; - } - @Transactional @Override public void applyOverdueChargesForLoan(final Long loanId, Collection overdueLoanScheduleDataList) { @@ -744,6 +737,40 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo } } + private LoanTransaction applyChargeAdjustment(final Loan loan, final LoanCharge loanCharge, final BigDecimal transactionAmount, + final LocalDate transactionDate, final ExternalId txnExternalId) { + businessEventNotifierService.notifyPreBusinessEvent(new LoanChargeAdjustmentPreBusinessEvent(loan)); + final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); + final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); + + LoanTransaction loanChargeAdjustmentTransaction = LoanTransaction.chargeAdjustment(loan, transactionAmount, transactionDate, + txnExternalId); + LoanTransactionRelation loanTransactionRelation = LoanTransactionRelation.linkToCharge(loanChargeAdjustmentTransaction, loanCharge, + LoanTransactionRelationTypeEnum.CHARGE_ADJUSTMENT); + loanChargeAdjustmentTransaction.getLoanTransactionRelations().add(loanTransactionRelation); + + loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(loanChargeAdjustmentTransaction); + + defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan); + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory + .determineProcessor(loan.transactionProcessingStrategy()); + loanRepaymentScheduleTransactionProcessor.handleTransaction(loanChargeAdjustmentTransaction, loan.getCurrency(), + loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); + + loan.addLoanTransaction(loanChargeAdjustmentTransaction); + loan.updateLoanSummaryAndStatus(); + + loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan); + + final Map accountingBridgeData = loan.deriveAccountingBridgeData(loan.getCurrency().getCode(), + existingTransactionIds, existingReversedTransactionIds, false); + this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); + + loanAccountDomainService.setLoanDelinquencyTag(loan, transactionDate); + + return loanChargeAdjustmentTransaction; + } + private void undoWaivedCharge(final Map changes, final Loan loan, final LoanTransaction loanTransaction, final LoanChargePaidBy loanChargePaidBy) { switch (loanChargePaidBy.getLoanCharge().getChargeTimeType()) { @@ -1057,7 +1084,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo jsonObject.addProperty("dateFormat", dateFormat); } DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateFormat); - LocalDate transactionDate = DateUtils.getLocalDateOfTenant(); + LocalDate transactionDate = DateUtils.getBusinessLocalDate(); String transactionDateString = transactionDate.format(dateTimeFormatter); jsonObject.addProperty("transactionDate", transactionDateString); if (!this.fromApiJsonHelper.parameterExists("transactionAmount", jsonObject)) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java index cc2103f70..73c7bfd4f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java @@ -22,6 +22,7 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.util.Collection; import java.util.List; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.infrastructure.core.service.SearchParameters; import org.apache.fineract.organisation.staff.data.StaffData; @@ -155,4 +156,7 @@ public interface LoanReadPlatformService { List retrieveLoanTransactionRelationsByLoanTransactionId(Long loanTransactionId); + Long retrieveLoanTransactionIdByExternalId(ExternalId externalId); + + Long retrieveLoanIdByExternalId(String externalId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 09f5cf98f..9ad07d2fe 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -41,6 +41,7 @@ import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.JdbcSupport; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.Page; @@ -1372,7 +1373,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final BigDecimal overPaymentPortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "overpayment"); final BigDecimal unrecognizedIncomePortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "unrecognizedIncome"); final BigDecimal outstandingLoanBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "outstandingLoanBalance"); - final String externalId = rs.getString("externalId"); + final String externalIdStr = rs.getString("externalId"); + final ExternalId externalId = StringUtils.isBlank(externalIdStr) ? ExternalId.empty() : new ExternalId(externalIdStr); final String reversalExternalId = rs.getString("reversalExternalId"); final LocalDate reversedOnDate = JdbcSupport.getLocalDate(rs, "reversedOnDate"); @@ -2186,7 +2188,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(loanTransactionType); return new LoanTransactionData(null, null, null, transactionType, null, currencyData, currentDate, transactionAmount, null, - netDisbursal, null, null, null, null, null, paymentOptions, null, null, null, null, false, loanId, externalLoanId); + netDisbursal, null, null, null, null, null, paymentOptions, ExternalId.empty(), null, null, null, false, loanId, + externalLoanId); } @Override @@ -2308,6 +2311,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { } public String schema() { + // TODO: investigate whether it can be refactored to be more efficient StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("(CASE "); sqlBuilder.append( @@ -2358,11 +2362,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final String officeName = null; boolean manuallyReversed = false; final PaymentDetailData paymentDetailData = null; - final String externalId = null; final AccountTransferData transfer = null; final BigDecimal fixedEmiAmount = null; return new LoanTransactionData(id, officeId, officeName, transactionType, paymentDetailData, currencyData, date, totalDue, - netDisbursalAmount, principalPortion, interestDue, feeDue, penaltyDue, overPaymentPortion, externalId, transfer, + netDisbursalAmount, principalPortion, interestDue, feeDue, penaltyDue, overPaymentPortion, ExternalId.empty(), transfer, fixedEmiAmount, outstandingLoanBalance, unrecognizedIncomePortion, manuallyReversed, loanId, externalLoanId); } @@ -2402,4 +2405,14 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { return loanTransactionRelationMapper.map(loanTransactionRelations); } + @Override + public Long retrieveLoanTransactionIdByExternalId(ExternalId externalId) { + return loanTransactionRepository.findIdByExternalId(externalId); + } + + @Override + public Long retrieveLoanIdByExternalId(String externalId) { + return loanRepositoryWrapper.findIdByExternalId(externalId); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 0d15b9d97..9c988127f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -49,11 +49,13 @@ 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.infrastructure.core.exception.PlatformServiceUnavailableException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum; import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService; @@ -244,6 +246,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private final LoanAccountLockService loanAccountLockService; + private final ExternalIdFactory externalIdFactory; + @Transactional @Override public CommandProcessingResult disburseGLIMLoan(final Long loanId, final JsonCommand command) { @@ -382,7 +386,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf Money disburseAmount = loan.adjustDisburseAmount(command, actualDisbursementDate); Money amountToDisburse = disburseAmount.copy(); boolean recalculateSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincipal()); - final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId"); + final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); if (loan.isTopup() && loan.getClientId() != null) { final Long loanIdToClose = loan.getTopupLoanDetails().getLoanIdToClose(); @@ -486,8 +490,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(actualDisbursementDate, entrySet.getValue(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, savingAccountData.getId(), loanId, "Loan Charge Payment", locale, fmt, null, null, LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.getValue(), entrySet.getKey(), null, - AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null, fromSavingsAccount, isRegularTransaction, - isExceptionForBalanceCheck); + AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, ExternalId.empty(), null, null, fromSavingsAccount, + isRegularTransaction, isExceptionForBalanceCheck); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); } @@ -505,17 +509,20 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalBusinessEvent(loan)); Long entityId = loan.getId(); + ExternalId externalId = ExternalId.empty(); // During a disbursement, the entityId should be the disbursement transaction id if (!isAccountTransfer) { LoanTransaction disbursalTransaction = loan.getLoanTransactions().get(loan.getLoanTransactions().size() - 1); entityId = disbursalTransaction.getId(); + externalId = disbursalTransaction.getExternalId(); businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalTransactionBusinessEvent(disbursalTransaction)); } return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(entityId) // + .withEntityExternalId(externalId) // .withOfficeId(loan.getOfficeId()) // .withClientId(loan.getClientId()) // .withGroupId(loan.getGroupId()) // @@ -608,7 +615,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { - baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique"); + baseDataValidator.reset().parameter(LoanApiConstants.externalIdParameterName).failWithCode("value.must.be.unique"); } if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", @@ -626,7 +633,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { - baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique"); + baseDataValidator.reset().parameter(LoanApiConstants.externalIdParameterName).failWithCode("value.must.be.unique"); } if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", @@ -687,7 +694,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf Money amountBeforeAdjust = loan.getPrincipal(); Money disburseAmount = loan.adjustDisburseAmount(command, actualDisbursementDate); boolean recalculateSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincipal()); - final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId"); + final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); if (isAccountTransfer) { disburseLoanToSavings(loan, command, disburseAmount, paymentDetail); existingTransactionIds.addAll(loan.findExistingTransactionIds()); @@ -753,7 +760,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(actualDisbursementDate, entrySet.getValue(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, savingAccountData.getId(), loan.getId(), "Loan Charge Payment", locale, fmt, null, null, LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.getValue(), - entrySet.getKey(), null, AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null, + entrySet.getKey(), null, AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, ExternalId.empty(), null, null, fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); } @@ -907,7 +914,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); - final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId"); + final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); final Map changes = new LinkedHashMap<>(); changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); @@ -920,15 +927,18 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (StringUtils.isNotBlank(noteText)) { changes.put("note", noteText); } + if (!txnExternalId.isEmpty()) { + changes.put(LoanApiConstants.externalIdParameterName, txnExternalId); + } final Loan loan = this.loanAssembler.assembleFrom(loanId); final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); final Boolean isHolidayValidationDone = false; final HolidayDetailDTO holidayDetailDto = null; boolean isAccountTransfer = false; - final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder(); - LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(repaymentTransactionType, loan, - commandProcessingResultBuilder, transactionDate, transactionAmount, paymentDetail, noteText, txnExternalId, - isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone); + + LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(repaymentTransactionType, loan, transactionDate, + transactionAmount, paymentDetail, noteText, txnExternalId, isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, + holidayDetailDto, isHolidayValidationDone); // Update loan transaction on repayment. if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { @@ -947,8 +957,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanAccountDomainService.updateLoanCollateralTransaction(loanCollateralManagements); } - return commandProcessingResultBuilder.withCommandId(command.commandId()) // - .withLoanId(loanId) // + return new CommandProcessingResultBuilder().withCommandId(command.commandId()) // + .withLoanId(loan.getId()) // + .withEntityId(loanTransaction.getId()) // + .withEntityExternalId(loanTransaction.getExternalId()) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // .with(changes) // .build(); } @@ -993,15 +1008,18 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (singleLoanRepaymentCommand != null) { final Loan loan = this.loanAssembler.assembleFrom(singleLoanRepaymentCommand.getLoanId()); final PaymentDetail paymentDetail = singleLoanRepaymentCommand.getPaymentDetail(); + ExternalId externalId = singleLoanRepaymentCommand.getExternalId(); + if (externalId.isEmpty() && configurationDomainService.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } if (paymentDetail != null && paymentDetail.getId() == null) { this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail); } - final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder(); final String chargeRefundChargeType = null; LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, loan, - commandProcessingResultBuilder, bulkRepaymentCommand.getTransactionDate(), - singleLoanRepaymentCommand.getTransactionAmount(), paymentDetail, bulkRepaymentCommand.getNote(), null, - isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDTO, isHolidayValidationDone); + bulkRepaymentCommand.getTransactionDate(), singleLoanRepaymentCommand.getTransactionAmount(), paymentDetail, + bulkRepaymentCommand.getNote(), externalId, isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, + holidayDetailDTO, isHolidayValidationDone); transactionIds.add(loanTransaction.getId()); } } @@ -1014,6 +1032,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Long transactionId, final JsonCommand command) { this.loanEventApiJsonValidator.validateTransaction(command.json()); + LoanTransaction transactionToAdjust = this.loanTransactionRepository.findByIdAndLoanId(command.entityId(), command.getLoanId()) + .orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId(), command.getLoanId())); Loan loan = this.loanAssembler.assembleFrom(loanId); if (loan.getStatus().isClosed() && loan.getLoanSubStatus() != null @@ -1022,8 +1042,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf throw new LoanForeclosureException("loan.cannot.be.reopened.as.it.is.foreclosured", defaultUserMessage, loanId); } checkClientOrGroupActive(loan); - final LoanTransaction transactionToAdjust = this.loanTransactionRepository.findById(transactionId) - .orElseThrow(() -> new LoanTransactionNotFoundException(transactionId)); + businessEventNotifierService.notifyPreBusinessEvent( new LoanAdjustTransactionBusinessEvent(new LoanAdjustTransactionBusinessEvent.Data(transactionToAdjust))); if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.LOAN)) { @@ -1049,8 +1068,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); - final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId"); - final String reversalExternalId = command.stringValueOfParameterNamedAllowingNull(LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME); + final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); + // TODO: can it be refactored to use the 'externalId' for reversal? + final ExternalId reversalTxnExternalId = externalIdFactory.createFromCommand(command, + LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME); final Map changes = new LinkedHashMap<>(); changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); @@ -1077,7 +1098,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } } newTransactionDetail = LoanTransaction.waiver(loan.getOffice(), loan, transactionAmountAsMoney, transactionDate, - interestComponent, unrecognizedIncome); + interestComponent, unrecognizedIncome, txnExternalId); } LocalDate recalculateFrom = null; @@ -1091,9 +1112,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final ChangedTransactionDetail changedTransactionDetail = loan.adjustExistingTransaction(newTransactionDetail, defaultLoanLifecycleStateMachine, transactionToAdjust, existingTransactionIds, existingReversedTransactionIds, - scheduleGeneratorDTO, reversalExternalId); + scheduleGeneratorDTO, reversalTxnExternalId); - if (newTransactionDetail.isGreaterThanZero(loan.getPrincipal().getCurrency())) { + boolean thereIsNewTransaction = newTransactionDetail.isGreaterThanZero(loan.getPrincipal().getCurrency()); + if (thereIsNewTransaction) { if (paymentDetail != null) { this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail); } @@ -1122,7 +1144,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf /** * If a new transaction is not created, associate note with the transaction to be adjusted **/ - if (newTransactionDetail.isGreaterThanZero(loan.getPrincipal().getCurrency())) { + if (thereIsNewTransaction) { note = Note.loanTransactionNote(loan, newTransactionDetail, noteText); } else { note = Note.loanTransactionNote(loan, transactionToAdjust, noteText); @@ -1151,14 +1173,27 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(transactionToAdjust); - if (newTransactionDetail.isRepaymentType() && newTransactionDetail.isGreaterThanZero(loan.getPrincipal().getCurrency())) { + if (newTransactionDetail.isRepaymentType() && thereIsNewTransaction) { eventData.setNewTransactionDetail(newTransactionDetail); } + Long entityId = transactionToAdjust.getId(); + ExternalId entityExternalId = transactionToAdjust.getExternalId(); + + if (thereIsNewTransaction) { + entityId = newTransactionDetail.getId(); + entityExternalId = newTransactionDetail.getExternalId(); + } businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(eventData)); - return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(transactionId) - .withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()).withGroupId(loan.getGroupId()).withLoanId(loanId) + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(entityId) // + .withEntityExternalId(entityExternalId) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(loanId) // .with(changes).build(); } @@ -1167,6 +1202,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf public CommandProcessingResult chargebackLoanTransaction(final Long loanId, final Long transactionId, final JsonCommand command) { this.loanEventApiJsonValidator.validateChargebackTransaction(command.json()); + LoanTransaction loanTransaction = this.loanTransactionRepository.findByIdAndLoanId(command.entityId(), command.getLoanId()) + .orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId(), command.getLoanId())); + Loan loan = this.loanAssembler.assembleFrom(loanId); if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.LOAN)) { throw new PlatformServiceUnavailableException("error.msg.loan.transfer.transaction.update.not.allowed", @@ -1187,9 +1225,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf checkClientOrGroupActive(loan); - LoanTransaction loanTransaction = this.loanTransactionRepository.findById(transactionId) - .orElseThrow(() -> new LoanTransactionNotFoundException(transactionId)); - if (loanTransaction.isReversed()) { throw new PlatformServiceUnavailableException("error.msg.loan.chargeback.operation.not.allowed", "Loan transaction:" + transactionId + " chargeback not allowed as loan transaction repayment is reversed", @@ -1207,7 +1242,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.TRANSACTION_AMOUNT_PARAMNAME); - final String txnExternalId = command.stringValueOfParameterNamedAllowingNull(LoanApiConstants.externalIdParameterName); + final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); final Map changes = new LinkedHashMap<>(); changes.put("transactionAmount", command.stringValueOfParameterNamed(LoanApiConstants.TRANSACTION_AMOUNT_PARAMNAME)); @@ -1247,8 +1282,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf businessEventNotifierService.notifyPostBusinessEvent(new LoanChargebackTransactionBusinessEvent(loanTransaction)); - return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(newTransaction.getId()) - .withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()).withGroupId(loan.getGroupId()).withLoanId(loanId) + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(newTransaction.getId()) // + .withEntityExternalId(newTransaction.getExternalId()) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(loanId) // .with(changes).build(); } @@ -1280,6 +1321,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf changes.put("dateFormat", command.dateFormat()); final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + final ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); @@ -1298,7 +1340,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } } final LoanTransaction waiveInterestTransaction = LoanTransaction.waiver(loan.getOffice(), loan, transactionAmountAsMoney, - transactionDate, interestComponent, unrecognizedIncome); + transactionDate, interestComponent, unrecognizedIncome, externalId); businessEventNotifierService.notifyPreBusinessEvent(new LoanWaiveInterestBusinessEvent(waiveInterestTransaction)); LocalDate recalculateFrom = null; if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { @@ -1339,8 +1381,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveInterestBusinessEvent(waiveInterestTransaction)); - return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(waiveInterestTransaction.getId()) - .withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()).withGroupId(loan.getGroupId()).withLoanId(loanId) + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(waiveInterestTransaction.getId()) // + .withEntityExternalId(waiveInterestTransaction.getExternalId()) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(loanId) // .with(changes).build(); } @@ -1404,8 +1452,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(writeOff)); - return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(writeOff.getId()) - .withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()).withGroupId(loan.getGroupId()).withLoanId(loanId) + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(writeOff.getId()) // + .withEntityExternalId(writeOff.getExternalId()) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(loanId) // .with(changes).build(); } @@ -1485,12 +1539,23 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf CommandProcessingResult result; if (possibleClosingTransaction != null) { - result = new CommandProcessingResultBuilder().withCommandId(command.commandId()) - .withEntityId(possibleClosingTransaction.getId()).withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()) - .withGroupId(loan.getGroupId()).withLoanId(loanId).with(changes).build(); + result = new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(possibleClosingTransaction.getId()) // + .withEntityExternalId(possibleClosingTransaction.getExternalId()) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(loanId) // + .with(changes).build(); } else { - result = new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loanId) - .withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()).withGroupId(loan.getGroupId()).withLoanId(loanId) + result = new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(loanId) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(loanId) // .with(changes).build(); } @@ -1558,7 +1623,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private void disburseLoanToLoan(final Loan loan, final JsonCommand command, final BigDecimal amount) { final LocalDate transactionDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); - final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId"); + final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); final Locale locale = command.extractLocale(); final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); @@ -1573,7 +1638,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private void disburseLoanToSavings(final Loan loan, final JsonCommand command, final Money amount, final PaymentDetail paymentDetail) { final LocalDate transactionDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); - final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId"); + final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); final Locale locale = command.extractLocale(); final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); @@ -1606,8 +1671,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); - - final LoanTransaction newTransferTransaction = LoanTransaction.initiateTransfer(loan.getOffice(), loan, transferDate); + ExternalId externalId = externalIdFactory.create(); + final LoanTransaction newTransferTransaction = LoanTransaction.initiateTransfer(loan.getOffice(), loan, transferDate, externalId); loan.addLoanTransaction(newTransferTransaction); LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine; loanLifecycleStateMachine.transition(LoanEvent.LOAN_INITIATE_TRANSFER, loan); @@ -1628,8 +1693,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf businessEventNotifierService.notifyPreBusinessEvent(new LoanAcceptTransferBusinessEvent(loan)); final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); - - final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.approveTransfer(acceptedInOffice, loan, transferDate); + ExternalId externalId = externalIdFactory.create(); + final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.approveTransfer(acceptedInOffice, loan, transferDate, + externalId); loan.addLoanTransaction(newTransferAcceptanceTransaction); LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine; if (loan.getTotalOverpaid() != null) { @@ -1659,7 +1725,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); - final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.withdrawTransfer(loan.getOffice(), loan, transferDate); + ExternalId externalId = externalIdFactory.create(); + + final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.withdrawTransfer(loan.getOffice(), loan, transferDate, + externalId); loan.addLoanTransaction(newTransferAcceptanceTransaction); LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine; loanLifecycleStateMachine.transition(LoanEvent.LOAN_WITHDRAW_TRANSFER, loan); @@ -2076,6 +2145,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf .withClientId(loan.getClientId()) // .withGroupId(loan.getGroupId()) // .withLoanId(loanId) // + .withEntityId(writeOffTransaction.getId()) // + .withEntityExternalId(writeOffTransaction.getExternalId()) // .build(); } @@ -2266,7 +2337,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf @Override public CommandProcessingResult recoverFromGuarantor(final Long loanId) { final Loan loan = this.loanAssembler.assembleFrom(loanId); - this.guarantorDomainService.transaferFundsFromGuarantor(loan); + this.guarantorDomainService.transferFundsFromGuarantor(loan); return new CommandProcessingResultBuilder().withLoanId(loanId).build(); } @@ -2333,10 +2404,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf public CommandProcessingResult creditBalanceRefund(Long loanId, JsonCommand command) { this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json()); + Loan loan = this.loanAssembler.assembleFrom(loanId); final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); final String noteText = command.stringValueOfParameterNamedAllowingNull("note"); - final String externalId = command.stringValueOfParameterNamedAllowingNull("externalId"); + final ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); final Map changes = new LinkedHashMap<>(); changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); @@ -2347,15 +2419,21 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (StringUtils.isNotBlank(noteText)) { changes.put("note", noteText); } - if (StringUtils.isNotBlank(externalId)) { - changes.put("externalId", externalId); + if (!externalId.isEmpty()) { + changes.put(LoanApiConstants.externalIdParameterName, externalId); } - final CommandProcessingResultBuilder commandProcessingResultBuilder = this.loanAccountDomainService.creditBalanceRefund(loanId, - transactionDate, transactionAmount, noteText, externalId); + final LoanTransaction loanTransaction = this.loanAccountDomainService.creditBalanceRefund(loan, transactionDate, transactionAmount, + noteText, externalId); - return commandProcessingResultBuilder // - .withCommandId(command.commandId()).with(changes) // + return new CommandProcessingResultBuilder() // + .withEntityId(loanTransaction.getId()) // + .withEntityExternalId(loanTransaction.getExternalId()) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withCommandId(command.commandId()) // + .with(changes) // .build(); } @@ -2398,6 +2476,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json()); final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); // checkRefundDateIsAfterAtLeastOneRepayment(loanId, transactionDate); @@ -2409,6 +2488,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount")); changes.put("locale", command.locale()); changes.put("dateFormat", command.dateFormat()); + changes.put(LoanApiConstants.externalIdParameterName, externalId); final String noteText = command.stringValueOfParameterNamed("note"); if (StringUtils.isNotBlank(noteText)) { @@ -2419,11 +2499,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder(); - this.loanAccountDomainService.makeRefundForActiveLoan(loanId, commandProcessingResultBuilder, transactionDate, transactionAmount, - paymentDetail, noteText, null); + LoanTransaction loanTransaction = this.loanAccountDomainService.makeRefundForActiveLoan(loanId, commandProcessingResultBuilder, + transactionDate, transactionAmount, paymentDetail, noteText, externalId); - return commandProcessingResultBuilder.withCommandId(command.commandId()) // + return commandProcessingResultBuilder // + .withCommandId(command.commandId()) // .withLoanId(loanId) // + .withEntityId(loanTransaction.getId()) // + .withEntityExternalId(loanTransaction.getExternalId()) // .with(changes) // .build(); @@ -2502,9 +2585,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final JsonElement element = fromApiJsonHelper.parse(json); final Loan loan = this.loanAssembler.assembleFrom(loanId); final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.transactionDateParamName, element); + final ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); this.loanEventApiJsonValidator.validateLoanForeclosure(command.json()); final Map changes = new LinkedHashMap<>(); changes.put("transactionDate", transactionDate); + changes.put("externalId", externalId); String noteText = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.noteParamName, element); LoanRescheduleRequest loanRescheduleRequest = null; @@ -2519,11 +2604,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanScheduleHistoryWritePlatformService.createAndSaveLoanScheduleArchive(loan.getRepaymentScheduleInstallments(), loan, loanRescheduleRequest); - final Map modifications = this.loanAccountDomainService.foreCloseLoan(loan, transactionDate, noteText); + final Map modifications = this.loanAccountDomainService.foreCloseLoan(loan, transactionDate, noteText, externalId); changes.putAll(modifications); final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder(); - return commandProcessingResultBuilder.withLoanId(loanId) // + return commandProcessingResultBuilder // + .withLoanId(loanId) // .with(changes) // .build(); } @@ -2566,5 +2652,4 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } } - } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java index 563c4fe5c..16e01e30c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java @@ -37,6 +37,7 @@ import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumb import org.apache.fineract.infrastructure.accountnumberformat.domain.EntityAccountType; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; @@ -242,8 +243,8 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi final boolean isExceptionForBalanceCheck = false; final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(closedDate, account.getAccountBalance(), PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, null, null, transferDescription, locale, fmt, null, null, - null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, null, null, toSavingsAccount, account, - isAccountTransfer, isExceptionForBalanceCheck); + null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, + toSavingsAccount, account, isAccountTransfer, isExceptionForBalanceCheck); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); updateAlreadyPostedTransactions(existingTransactionIds, account); postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); @@ -315,8 +316,8 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi final boolean isExceptionForBalanceCheck = false; final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(closedDate, account.getAccountBalance(), PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, null, null, transferDescription, null, fmt, null, null, - null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, null, null, toSavingsAccount, account, - isAccountTransfer, isExceptionForBalanceCheck); + null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, + toSavingsAccount, account, isAccountTransfer, isExceptionForBalanceCheck); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); updateAlreadyPostedTransactions(existingTransactionIds, account); account.updateClosedStatus(); @@ -400,8 +401,8 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi final boolean isExceptionForBalanceCheck = false; final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(closedDate, transactionAmount, PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, null, null, transferDescription, locale, fmt, null, null, - null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, null, null, toSavingsAccount, account, - isRegularTransaction, isExceptionForBalanceCheck); + null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, + toSavingsAccount, account, isRegularTransaction, isExceptionForBalanceCheck); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); updateAlreadyPostedTransactions(existingTransactionIds, account); } else { @@ -491,8 +492,8 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi DepositAccountType.SAVINGS_DEPOSIT); final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(closedDate, account.getAccountBalance(), PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, null, null, transferDescription, locale, fmt, null, null, - null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, null, null, toSavingsAccount, account, - isRegularTransaction, isExceptionForBalanceCheck); + null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, + toSavingsAccount, account, isRegularTransaction, isExceptionForBalanceCheck); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); updateAlreadyPostedTransactions(existingTransactionIds, account); } else { @@ -547,8 +548,8 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi DepositAccountType.SAVINGS_DEPOSIT); final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(closedDate, account.getAccountBalance(), PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, null, null, transferDescription, locale, fmt, null, null, - null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, null, null, toSavingsAccount, account, - isRegularTransaction, isExceptionForBalanceCheck); + null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, + toSavingsAccount, account, isRegularTransaction, isExceptionForBalanceCheck); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); updateAlreadyPostedTransactions(existingTransactionIds, account); } else { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java index 5881aca22..c968a691c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java @@ -32,6 +32,7 @@ import java.util.Set; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.data.PaginationParameters; import org.apache.fineract.infrastructure.core.data.PaginationParametersDataValidator; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.JdbcSupport; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.Page; @@ -1700,8 +1701,8 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead final LocalDate transactionDate = JdbcSupport.getLocalDate(rs, TRANSACTION_DATE); return new AccountTransferDTO(transactionDate, transactionAmount, PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, fromAccountId, toAccountId, TRANSFER_INTEREST_TO_SAVINGS, null, null, null, null, null, null, null, - AccountTransferType.INTEREST_TRANSFER.getValue(), null, null, null, null, null, null, isRegularTransaction, - isExceptionForBalanceCheck); + AccountTransferType.INTEREST_TRANSFER.getValue(), null, null, ExternalId.empty(), null, null, null, + isRegularTransaction, isExceptionForBalanceCheck); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java index 58c5f6df7..e2a194fe7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java @@ -47,6 +47,7 @@ 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.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -181,8 +182,8 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(account.getActivationLocalDate(), amountForDeposit.getAmount(), PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, portfolioAccountData.getId(), account.getId(), "Account Transfer", locale, fmt, null, null, null, null, null, - AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, null, null, account, fromSavingsAccount, - isRegularTransaction, isExceptionForBalanceCheck); + AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, account, + fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); } final boolean isInterestTransfer = false; @@ -285,8 +286,8 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(account.getActivationLocalDate(), amountForDeposit.getAmount(), PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, portfolioAccountData.getId(), account.getId(), "Account Transfer", locale, fmt, null, null, null, null, null, - AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, null, null, account, fromSavingsAccount, - isRegularTransaction, isExceptionForBalanceCheck); + AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, account, + fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); } updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); 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 ac677cd75..e6719b6aa 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 @@ -90,4 +90,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0071_add_external_id_support_for_loan_transaction.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0071_add_external_id_support_for_loan_transaction.xml new file mode 100644 index 000000000..d864e6b7e --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0071_add_external_id_support_for_loan_transaction.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java index d06b5a878..daa4043d6 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java @@ -5913,6 +5913,7 @@ public class ClientLoanIntegrationTest { this.loanTransactionHelper.makeRepayment(loanPrepaymentDate, Float.parseFloat(prepayAmount), loanID); loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); LoanStatusChecker.verifyLoanAccountIsClosed(loanStatusHashMap); + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "42", false); } @Test @@ -5984,6 +5985,7 @@ public class ClientLoanIntegrationTest { HashMap savingsAccountErrorData = validationErrorHelper.makeRepayment(retrieveDueDate, amount, loanID); ArrayList error = (ArrayList) savingsAccountErrorData.get("errors"); assertEquals("error.msg.loan.transaction.cannot.be.a.future.date", error.get(0).get("userMessageGlobalisationCode")); + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "42", false); } @Test @@ -6016,7 +6018,8 @@ public class ClientLoanIntegrationTest { ResponseSpecification responseSpec = new ResponseSpecBuilder().expectStatusCode(404).build(); LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, responseSpec); HashMap response = loanTransactionHelper.undoWaiveChargesForLoan(-1, -2, ""); - assertEquals("error.msg.loan.id.invalid", ((Map) ((List) response.get("errors")).get(0)).get("userMessageGlobalisationCode")); + assertEquals("error.msg.loan.transaction.id.invalid", + ((Map) ((List) response.get("errors")).get(0)).get("userMessageGlobalisationCode")); assertEquals("Transaction with identifier -2 does not exist for loan with identifier -1.", ((Map) ((List) response.get("errors")).get(0)).get("defaultUserMessage")); } @@ -6176,13 +6179,13 @@ public class ClientLoanIntegrationTest { assertEquals(1000.0f, loanSummary.get("totalOutstanding")); GetLoansLoanIdTransactionsTransactionIdResponse chargeAdjustmentTransaction = this.loanTransactionHelper - .getLoanTransactionDetails((long) loanID, chargeAdjustmentResponse.getResourceId()); + .getLoanTransactionDetails((long) loanID, chargeAdjustmentResponse.getSubResourceId()); assertEquals(10.0, chargeAdjustmentTransaction.getAmount()); assertEquals(10.0, chargeAdjustmentTransaction.getPenaltyChargesPortion()); assertEquals("loanTransactionType.chargeAdjustment", chargeAdjustmentTransaction.getType().getCode()); assertEquals(externalId, chargeAdjustmentTransaction.getExternalId()); GetLoanTransactionRelation transactionRelation = chargeAdjustmentTransaction.getTransactionRelations().iterator().next(); - assertEquals(chargeAdjustmentResponse.getResourceId(), transactionRelation.getFromLoanTransaction()); + assertEquals(chargeAdjustmentResponse.getSubResourceId(), transactionRelation.getFromLoanTransaction()); assertEquals((long) penalty1LoanChargeId, transactionRelation.getToLoanCharge()); assertEquals("CHARGE_ADJUSTMENT", transactionRelation.getRelationType()); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalIdSupportIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalIdSupportIntegrationTest.java new file mode 100644 index 000000000..5b3e5e814 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalIdSupportIntegrationTest.java @@ -0,0 +1,431 @@ +/** + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +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.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.HashMap; +import java.util.UUID; +import org.apache.fineract.client.models.PostClientsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdRequest; +import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; +import org.apache.fineract.integrationtests.client.IntegrationTest; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; +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.charges.ChargesHelper; +import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class ExternalIdSupportIntegrationTest extends IntegrationTest { + + private ResponseSpecification responseSpec; + private RequestSpecification requestSpec; + + private ClientHelper clientHelper; + private AccountHelper accountHelper; + private LoanTransactionHelper loanTransactionHelper; + + private DateTimeFormatter dateFormatter = new DateTimeFormatterBuilder().appendPattern("dd MMMM yyyy").toFormatter(); + + @BeforeAll + public void init() { + Utils.initializeRESTAssured(); + this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec); + this.accountHelper = new AccountHelper(this.requestSpec, this.responseSpec); + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + } + + @Test + public void test() { + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, 50, true); + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account assetFeeAndPenaltyAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", true)); + + Integer penalty2 = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", true, 1)); + + final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("1000").withRepaymentTypeAsMonth() + .withRepaymentAfterEvery("1").withNumberOfRepayments("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("0") + .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat() + .withAccountingRulePeriodicAccrual(new Account[] { assetAccount, incomeAccount, expenseAccount, overpaymentAccount }) + .withDaysInMonth("30").withDaysInYear("365").withMoratorium("0", "0") + .withFeeAndPenaltyAssetAccount(assetFeeAndPenaltyAccount).build(null); + final Integer loanProductID = this.loanTransactionHelper.getLoanProductId(loanProductJSON); + + final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + + String loanExternalIdStr = UUID.randomUUID().toString(); + final HashMap loan = applyForLoanApplication(client.getClientId().intValue(), loanProductID, loanExternalIdStr); + Integer loanId = (Integer) loan.get("resourceId"); + + this.loanTransactionHelper.approveLoan("02 September 2022", loanId); + String txnExternalIdStr = UUID.randomUUID().toString(); + final HashMap disbursedLoanResult = this.loanTransactionHelper.disburseLoan("03 September 2022", loanId, "1000", txnExternalIdStr); + + // Check whether the provided external id was retrieved + assertEquals(txnExternalIdStr, disbursedLoanResult.get("resourceExternalId")); + + LocalDate targetDate = LocalDate.of(2022, 9, 7); + final String penaltyCharge1AddedDate = dateFormatter.format(targetDate); + + Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "10")); + + Integer penalty2LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "10")); + + // Check whether we can fetch transaction templates with proper result http code (HTTP 200..300) + ok(fineract().loanTransactions.retrieveTransactionTemplate1(loanExternalIdStr, "repayment", null, null, null)); + ok(fineract().loanTransactions.retrieveTransactionTemplate1(loanExternalIdStr, "payoutRefund", null, null, null)); + ok(fineract().loanTransactions.retrieveTransactionTemplate1(loanExternalIdStr, "waiveinterest", null, null, null)); + ok(fineract().loanTransactions.retrieveTransactionTemplate1(loanExternalIdStr, "close-rescheduled", null, null, null)); + ok(fineract().loanTransactions.retrieveTransactionTemplate1(loanExternalIdStr, "disburse", null, null, null)); + ok(fineract().loanTransactions.retrieveTransactionTemplate1(loanExternalIdStr, "recoverypayment", null, null, null)); + ok(fineract().loanTransactions.retrieveTransactionTemplate1(loanExternalIdStr, "refundbycash", null, null, null)); + ok(fineract().loanTransactions.retrieveTransactionTemplate1(loanExternalIdStr, "foreclosure", null, null, null)); + ok(fineract().loanTransactions.retrieveTransactionTemplate1(loanExternalIdStr, "creditBalanceRefund", null, null, null)); + + // Check whether an external id was generated + String waiveChargeExternalIdStr = UUID.randomUUID().toString(); + PostLoansLoanIdChargesChargeIdResponse waiveLoanChargeResult = loanTransactionHelper.waiveLoanCharge((long) loanId, + (long) penalty1LoanChargeId, new PostLoansLoanIdChargesChargeIdRequest().externalId(waiveChargeExternalIdStr)); + assertEquals(waiveChargeExternalIdStr, waiveLoanChargeResult.getSubResourceExternalId()); + + // Check whether an external id was generated + HashMap undoWaiveLoanChargeResult = loanTransactionHelper.undoWaiveLoanCharge((long) loanId, waiveChargeExternalIdStr); + assertEquals(waiveChargeExternalIdStr, undoWaiveLoanChargeResult.get("subResourceExternalId")); + + // Check whether an external id was generated + waiveLoanChargeResult = loanTransactionHelper.waiveLoanCharge((long) loanId, (long) penalty1LoanChargeId, + new PostLoansLoanIdChargesChargeIdRequest()); + assertNotNull(waiveLoanChargeResult.getSubResourceExternalId()); + + // Check whether an external id was generated + undoWaiveLoanChargeResult = loanTransactionHelper.undoWaiveLoanCharge(loanExternalIdStr, waiveLoanChargeResult.getSubResourceId()); + assertNotNull(undoWaiveLoanChargeResult.get("subResourceExternalId")); + + // Check whether an external id was generated + waiveChargeExternalIdStr = UUID.randomUUID().toString(); + waiveLoanChargeResult = loanTransactionHelper.waiveLoanCharge((long) loanId, (long) penalty1LoanChargeId, + new PostLoansLoanIdChargesChargeIdRequest().externalId(waiveChargeExternalIdStr)); + assertEquals(waiveChargeExternalIdStr, waiveLoanChargeResult.getSubResourceExternalId()); + + // Check whether an external id was generated + undoWaiveLoanChargeResult = loanTransactionHelper.undoWaiveLoanCharge(loanExternalIdStr, + waiveLoanChargeResult.getSubResourceExternalId()); + assertEquals(waiveChargeExternalIdStr, undoWaiveLoanChargeResult.get("subResourceExternalId")); + + // Check whether an external id was generated + final PostLoansLoanIdTransactionsResponse repaymentResult = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("06 September 2022").locale("en") + .transactionAmount(5.0)); + assertNotNull(repaymentResult.getResourceExternalId()); + + // Check whether the provided external id was retrieved + String transactionExternalIdStr = UUID.randomUUID().toString(); + final PostLoansLoanIdTransactionsResponse repaymentResultWithExternalId = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("06 September 2022").locale("en") + .transactionAmount(5.0).externalId(transactionExternalIdStr)); + assertEquals(transactionExternalIdStr, repaymentResultWithExternalId.getResourceExternalId()); + + // Check whether an external id was generated + final PostLoansLoanIdTransactionsResponse merchantIssuedRefundResult = loanTransactionHelper + .makeMerchantIssuedRefund(loanExternalIdStr, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate("06 September 2022").locale("en").transactionAmount(5.0)); + assertNotNull(merchantIssuedRefundResult.getResourceExternalId()); + + // Check whether the provided external id was retrieved + transactionExternalIdStr = UUID.randomUUID().toString(); + final PostLoansLoanIdTransactionsResponse merchantIssuedRefundResultWithExternalId = loanTransactionHelper + .makeMerchantIssuedRefund(loanExternalIdStr, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate("06 September 2022").locale("en").transactionAmount(5.0).externalId(transactionExternalIdStr)); + assertEquals(transactionExternalIdStr, merchantIssuedRefundResultWithExternalId.getResourceExternalId()); + + // Check whether an external id was generated + final PostLoansLoanIdTransactionsResponse payoutRefundResult = loanTransactionHelper.makePayoutRefund(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("06 September 2022").locale("en") + .transactionAmount(5.0)); + assertNotNull(payoutRefundResult.getResourceExternalId()); + + // Check whether the provided external id was retrieved + transactionExternalIdStr = UUID.randomUUID().toString(); + final PostLoansLoanIdTransactionsResponse payoutRefundResultWithExternalId = loanTransactionHelper + .makePayoutRefund(loanExternalIdStr, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate("06 September 2022").locale("en").transactionAmount(5.0).externalId(transactionExternalIdStr)); + assertEquals(transactionExternalIdStr, payoutRefundResultWithExternalId.getResourceExternalId()); + + // Check whether an external id was generated + final PostLoansLoanIdTransactionsResponse goodWillCreditResult = loanTransactionHelper.makeGoodwillCredit(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("06 September 2022").locale("en") + .transactionAmount(5.0)); + assertNotNull(goodWillCreditResult.getResourceExternalId()); + + // Check whether the provided external id was retrieved + transactionExternalIdStr = UUID.randomUUID().toString(); + final PostLoansLoanIdTransactionsResponse goodWillCreditResultWithExternalId = loanTransactionHelper + .makeGoodwillCredit(loanExternalIdStr, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate("06 September 2022").locale("en").transactionAmount(5.0).externalId(transactionExternalIdStr)); + assertEquals(transactionExternalIdStr, goodWillCreditResultWithExternalId.getResourceExternalId()); + + // Check whether an external id was generated + final PostLoansLoanIdTransactionsResponse writeoffResult = loanTransactionHelper.makeWriteoff(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("06 September 2022").locale("en")); + assertNotNull(writeoffResult.getResourceExternalId()); + + // Check whether an external id was generated + final PostLoansLoanIdTransactionsResponse makeRecoveryPaymentResult = loanTransactionHelper.makeRecoveryPayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("06 September 2022").locale("en") + .transactionAmount(5.0)); + assertNotNull(makeRecoveryPaymentResult.getResourceExternalId()); + + // Check whether the provided external id was retrieved + transactionExternalIdStr = UUID.randomUUID().toString(); + final PostLoansLoanIdTransactionsResponse makeRecoveryPaymentResultWithExternalId = loanTransactionHelper + .makeRecoveryPayment(loanExternalIdStr, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate("06 September 2022").locale("en").transactionAmount(5.0).externalId(transactionExternalIdStr)); + assertEquals(transactionExternalIdStr, makeRecoveryPaymentResultWithExternalId.getResourceExternalId()); + + // Check whether an external id was generated + final PostLoansLoanIdTransactionsResponse undoWriteoffResult = loanTransactionHelper.makeUndoWriteoff(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("06 September 2022").locale("en") + .transactionAmount(5.0)); + assertNotNull(undoWriteoffResult.getResourceExternalId()); + + // Check whether the provided external id was retrieved + transactionExternalIdStr = UUID.randomUUID().toString(); + final PostLoansLoanIdTransactionsResponse writeoffResultWithExternalId = loanTransactionHelper.makeWriteoff(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("06 September 2022").locale("en") + .externalId(transactionExternalIdStr)); + assertEquals(transactionExternalIdStr, writeoffResultWithExternalId.getResourceExternalId()); + + // Check whether the provided external id was retrieved + final PostLoansLoanIdTransactionsResponse undoWriteoffResultWithExternalId = loanTransactionHelper + .makeUndoWriteoff(loanExternalIdStr, new PostLoansLoanIdTransactionsRequest()); + assertEquals(transactionExternalIdStr, undoWriteoffResultWithExternalId.getResourceExternalId()); + + // Overpay the account + transactionExternalIdStr = UUID.randomUUID().toString(); + final PostLoansLoanIdTransactionsResponse overpaymentResultWithExternalId = loanTransactionHelper + .makeLoanRepayment(loanExternalIdStr, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate("06 September 2022").locale("en").transactionAmount(5000.0).externalId(transactionExternalIdStr)); + assertEquals(transactionExternalIdStr, overpaymentResultWithExternalId.getResourceExternalId()); + + // Check whether an external id was generated + final PostLoansLoanIdTransactionsResponse makeCreditBalanceRefundResult = loanTransactionHelper + .makeCreditBalanceRefund(loanExternalIdStr, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate("06 September 2022").locale("en").transactionAmount(5.0)); + assertNotNull(makeCreditBalanceRefundResult.getResourceExternalId()); + + // Check whether the provided external id was retrieved + transactionExternalIdStr = UUID.randomUUID().toString(); + final PostLoansLoanIdTransactionsResponse makeCreditBalanceRefundResultWithExternalId = loanTransactionHelper + .makeCreditBalanceRefund(loanExternalIdStr, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate("06 September 2022").locale("en").transactionAmount(5.0).externalId(transactionExternalIdStr)); + assertEquals(transactionExternalIdStr, makeCreditBalanceRefundResultWithExternalId.getResourceExternalId()); + + // Check whether an external id was generated + final PostLoansLoanIdTransactionsResponse chargeRefundResult = loanTransactionHelper.makeChargeRefund(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").locale("en").loanChargeId(penalty1LoanChargeId) + .transactionAmount(1.0)); + assertNotNull(chargeRefundResult.getResourceExternalId()); + + // Check whether the provided external id was retrieved + transactionExternalIdStr = UUID.randomUUID().toString(); + final PostLoansLoanIdTransactionsResponse chargeRefundResultWithExternalId = loanTransactionHelper + .makeChargeRefund(loanExternalIdStr, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").locale("en") + .loanChargeId(penalty1LoanChargeId).transactionAmount(1.0).externalId(transactionExternalIdStr)); + assertEquals(transactionExternalIdStr, chargeRefundResultWithExternalId.getResourceExternalId()); + + // Create a loan with interest and test the rest of the transactions + + final String loanProductWithInterestJSON = new LoanProductTestBuilder().withPrincipal("10000.0").withRepaymentTypeAsMonth() + .withRepaymentAfterEvery("2").withNumberOfRepayments("5").withRepaymentTypeAsMonth().withinterestRatePerPeriod("1") + .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat() + .withAccounting("1", null).build(null); + final Integer loanProductWithInterestID = this.loanTransactionHelper.getLoanProductId(loanProductWithInterestJSON); + + LocalDate aMonthBefore = Utils.getLocalDateOfTenant().minusMonths(1); + String formattedDate = dateFormatter.format(aMonthBefore); + + final Integer savingsId = SavingsAccountHelper.openSavingsAccount(this.requestSpec, this.responseSpec, + client.getClientId().intValue(), "10000.0"); + + loanExternalIdStr = UUID.randomUUID().toString(); + final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("10000.0").withLoanTermFrequency("10") + .withLoanTermFrequencyAsMonths().withNumberOfRepayments("5").withRepaymentEveryAfter("2") + .withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("1").withInterestTypeAsFlatBalance() + .withAmortizationTypeAsEqualPrincipalPayments().withInterestCalculationPeriodTypeSameAsRepaymentPeriod() + .withExpectedDisbursementDate(formattedDate).withSubmittedOnDate(formattedDate).withLoanType("individual") + .withExternalId(loanExternalIdStr) + .build(client.getClientId().toString(), loanProductWithInterestID.toString(), savingsId.toString()); + final HashMap loanWithInterest = this.loanTransactionHelper.getLoanId(loanApplicationJSON, ""); + Integer loanWithInterestId = (Integer) loanWithInterest.get("resourceId"); + + this.loanTransactionHelper.approveLoan(formattedDate, loanWithInterestId); + final HashMap disbursedLoanWithInterestResult = this.loanTransactionHelper.disburseLoan(formattedDate, loanWithInterestId, "1000", + null); + // Check whether an external id was generated + assertNotNull(disbursedLoanWithInterestResult.get("resourceExternalId")); + LocalDate aMonthBeforePlus3Days = aMonthBefore.plusDays(3); + formattedDate = dateFormatter.format(aMonthBeforePlus3Days); + + Integer penalty3LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanWithInterestId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), formattedDate, "10")); + + Integer penalty4LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanWithInterestId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty2), formattedDate, "1000")); + + Integer penalty5LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanWithInterestId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty2), formattedDate, "1000")); + + // Check whether an external id was generated + final PostLoansLoanIdTransactionsResponse waiveInterestResult = loanTransactionHelper.makeWaiveInterest(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate(formattedDate).locale("en") + .transactionAmount(5.0)); + assertNotNull(waiveInterestResult.getResourceExternalId()); + + // Check whether the provided external id was retrieved + String waiveInterestTxnExternalIdStr = UUID.randomUUID().toString(); + final PostLoansLoanIdTransactionsResponse waiveInterestResultWithExternalId = loanTransactionHelper + .makeWaiveInterest(loanExternalIdStr, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate(formattedDate).locale("en").transactionAmount(1.0).externalId(waiveInterestTxnExternalIdStr)); + assertEquals(waiveInterestTxnExternalIdStr, waiveInterestResultWithExternalId.getResourceExternalId()); + + String inAdvanceRepaymentTxnExternalIdStr = UUID.randomUUID().toString(); + final PostLoansLoanIdTransactionsResponse inAdvanceRepaymentResult = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate(formattedDate).locale("en") + .transactionAmount(500.0).externalId(inAdvanceRepaymentTxnExternalIdStr)); + + String inAdvanceRepayment2TxnExternalIdStr = UUID.randomUUID().toString(); + final PostLoansLoanIdTransactionsResponse inAdvanceRepayment2Result = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate(formattedDate).locale("en") + .transactionAmount(50.0).externalId(inAdvanceRepayment2TxnExternalIdStr)); + + // Check whether an external id was generated + final PostLoansLoanIdTransactionsResponse makeRefundByCashResult = loanTransactionHelper.makeRefundByCash(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate(formattedDate).locale("en") + .transactionAmount(1.0)); + assertNotNull(makeRefundByCashResult.getResourceExternalId()); + + // Check whether the provided external id was retrieved + String makeRefundTxnExternalIdStr = UUID.randomUUID().toString(); + final PostLoansLoanIdTransactionsResponse makeRefundByCashResultWithExternalId = loanTransactionHelper + .makeRefundByCash(loanExternalIdStr, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") + .transactionDate(formattedDate).locale("en").transactionAmount(5.0).externalId(makeRefundTxnExternalIdStr)); + assertEquals(makeRefundTxnExternalIdStr, makeRefundByCashResultWithExternalId.getResourceExternalId()); + + PostLoansLoanIdTransactionsResponse adjustmentResult = loanTransactionHelper.reverseLoanTransaction((long) loanWithInterestId, + inAdvanceRepayment2TxnExternalIdStr, new PostLoansLoanIdTransactionsTransactionIdRequest().transactionDate(formattedDate) + .locale("en").dateFormat("dd MMMM yyyy").transactionAmount(0.0)); + assertEquals(inAdvanceRepayment2TxnExternalIdStr, adjustmentResult.getResourceExternalId()); + + adjustmentResult = loanTransactionHelper.reverseLoanTransaction(loanExternalIdStr, inAdvanceRepaymentResult.getResourceId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().transactionDate(formattedDate).locale("en").dateFormat("dd MMMM yyyy") + .transactionAmount(0.0)); + assertEquals(inAdvanceRepaymentTxnExternalIdStr, adjustmentResult.getResourceExternalId()); + + String adjustTransactionExternalId = UUID.randomUUID().toString(); + adjustmentResult = loanTransactionHelper.adjustLoanTransaction(loanExternalIdStr, waiveInterestTxnExternalIdStr, + new PostLoansLoanIdTransactionsTransactionIdRequest().transactionDate(formattedDate).locale("en").dateFormat("dd MMMM yyyy") + .transactionAmount(2.0).externalId(adjustTransactionExternalId)); + assertEquals(adjustTransactionExternalId, adjustmentResult.getResourceExternalId()); + + adjustmentResult = loanTransactionHelper.adjustLoanTransaction(loanExternalIdStr, adjustmentResult.getResourceExternalId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().transactionDate(formattedDate).locale("en").dateFormat("dd MMMM yyyy") + .transactionAmount(1.0)); + assertNotNull(adjustmentResult.getResourceExternalId()); + + String repaymentForChargeback = UUID.randomUUID().toString(); + final PostLoansLoanIdTransactionsResponse repaymentForChargebackResult = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate(formattedDate).locale("en") + .transactionAmount(5.0).externalId(repaymentForChargeback)); + + String chargebackTransactionExternalId = UUID.randomUUID().toString(); + PostLoansLoanIdTransactionsResponse chargebackResult = loanTransactionHelper.chargebackLoanTransaction(loanExternalIdStr, + repaymentForChargebackResult.getResourceExternalId(), new PostLoansLoanIdTransactionsTransactionIdRequest().locale("en") + .transactionAmount(2.0).externalId(chargebackTransactionExternalId).paymentTypeId(1L)); + assertEquals(chargebackTransactionExternalId, chargebackResult.getResourceExternalId()); + + chargebackResult = loanTransactionHelper.chargebackLoanTransaction(loanExternalIdStr, + repaymentForChargebackResult.getResourceExternalId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().locale("en").transactionAmount(2.0).paymentTypeId(2L)); + assertNotNull(chargebackResult.getResourceExternalId()); + + // Check whether the provided external id was retrieved + String chargeAdjustmentExternalIdStr = UUID.randomUUID().toString(); + PostLoansLoanIdChargesChargeIdResponse chargeAdjustmentResult = loanTransactionHelper.chargeAdjustment((long) loanWithInterestId, + (long) penalty3LoanChargeId, + new PostLoansLoanIdChargesChargeIdRequest().externalId(chargeAdjustmentExternalIdStr).amount(1.0).locale("en")); + assertEquals(chargeAdjustmentExternalIdStr, chargeAdjustmentResult.getSubResourceExternalId()); + + // Check whether an external id was generated + chargeAdjustmentResult = loanTransactionHelper.chargeAdjustment((long) loanWithInterestId, (long) penalty3LoanChargeId, + new PostLoansLoanIdChargesChargeIdRequest().amount(1.0).locale("en")); + assertNotNull(chargeAdjustmentResult.getSubResourceExternalId()); + + HashMap payChargeResult = this.loanTransactionHelper.payChargesForLoan(loanWithInterestId, penalty4LoanChargeId, + LoanTransactionHelper.getPayChargeJSON(formattedDate, null), ""); + assertNotNull(payChargeResult.get("subResourceExternalId")); + + String payChargeExternalIdStr = UUID.randomUUID().toString(); + payChargeResult = this.loanTransactionHelper.payChargesForLoan(loanWithInterestId, penalty5LoanChargeId, + LoanTransactionHelper.getPayChargeJSON(formattedDate, null, payChargeExternalIdStr), ""); + assertEquals(payChargeExternalIdStr, payChargeResult.get("subResourceExternalId")); + + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, 50, false); + } + + private HashMap applyForLoanApplication(final Integer clientID, final Integer loanProductID, final String externalId) { + final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("1") + .withLoanTermFrequencyAsMonths().withNumberOfRepayments("1").withRepaymentEveryAfter("1") + .withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("0").withInterestTypeAsFlatBalance() + .withAmortizationTypeAsEqualPrincipalPayments().withInterestCalculationPeriodTypeSameAsRepaymentPeriod() + .withExpectedDisbursementDate("03 September 2022").withSubmittedOnDate("01 September 2022").withLoanType("individual") + .withExternalId(externalId).build(clientID.toString(), loanProductID.toString(), null); + return (HashMap) this.loanTransactionHelper.createLoanAccount(loanApplicationJSON, ""); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAuditingIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAuditingIntegrationTest.java index 05396c2ed..9da99fa1a 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAuditingIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAuditingIntegrationTest.java @@ -75,7 +75,7 @@ public class LoanAuditingIntegrationTest { public void checkAuditDates() throws InterruptedException { final Integer staffId = StaffHelper.createStaff(this.requestSpec, this.responseSpec); String username = Utils.randomNameGenerator("user", 8); - final Integer userId = (Integer) UserHelper.createUser(this.requestSpec, this.responseSpec, 1, staffId, username, "password", + final Integer userId = (Integer) UserHelper.createUser(this.requestSpec, this.responseSpec, 1, staffId, username, "P4ssw0rd", "resourceId"); LOG.info("-------------------------Creating Client---------------------------"); @@ -129,7 +129,7 @@ public class LoanAuditingIntegrationTest { this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); this.requestSpec.header("Authorization", - "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(username, "password")); + "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(username, "P4ssw0rd")); this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); loanStatusHashMap = this.loanTransactionHelper.approveLoan("11 July 2022", loanID); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAuditingIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAuditingIntegrationTest.java index 4817046e2..66e0a1d39 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAuditingIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAuditingIntegrationTest.java @@ -74,7 +74,7 @@ public class LoanTransactionAuditingIntegrationTest { public void checkAuditDates() throws InterruptedException { final Integer staffId = StaffHelper.createStaff(this.requestSpec, this.responseSpec); String username = Utils.randomNameGenerator("user", 8); - final Integer userId = (Integer) UserHelper.createUser(this.requestSpec, this.responseSpec, 1, staffId, username, "password", + final Integer userId = (Integer) UserHelper.createUser(this.requestSpec, this.responseSpec, 1, staffId, username, "P4ssw0rd", "resourceId"); LOG.info("-------------------------Creating Client---------------------------"); @@ -138,7 +138,7 @@ public class LoanTransactionAuditingIntegrationTest { this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); this.requestSpec.header("Authorization", - "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(username, "password")); + "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(username, "P4ssw0rd")); this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); this.loanTransactionHelper.reverseRepayment(loanID, transactionId, "11 July 2022"); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java index c110b3dac..4b3866ea7 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java @@ -98,9 +98,9 @@ public class GlobalConfigurationHelper { ArrayList expectedGlobalConfigurations = getAllDefaultGlobalConfigurations(); ArrayList actualGlobalConfigurations = getAllGlobalConfigurations(requestSpec, responseSpec); - // There are currently 37 global configurations. - Assertions.assertEquals(44, expectedGlobalConfigurations.size()); - Assertions.assertEquals(44, actualGlobalConfigurations.size()); + // There are currently 45 global configurations. + Assertions.assertEquals(45, expectedGlobalConfigurations.size()); + Assertions.assertEquals(45, actualGlobalConfigurations.size()); for (int i = 0; i < expectedGlobalConfigurations.size(); i++) { @@ -488,6 +488,14 @@ public class GlobalConfigurationHelper { loanRepaymentOverdueDaysDefault.put("trapDoor", false); defaults.add(loanRepaymentOverdueDaysDefault); + HashMap isAutomaticExternalIdGenerationEnabled = new HashMap<>(); + isAutomaticExternalIdGenerationEnabled.put("id", 50); + isAutomaticExternalIdGenerationEnabled.put("name", "enable-auto-generated-external-id"); + isAutomaticExternalIdGenerationEnabled.put("value", 0); + isAutomaticExternalIdGenerationEnabled.put("enabled", false); + isAutomaticExternalIdGenerationEnabled.put("trapDoor", false); + defaults.add(isAutomaticExternalIdGenerationEnabled); + return defaults; } 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 33f53a2d6..d499bfc14 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 @@ -58,7 +58,6 @@ import org.apache.fineract.client.models.PostLoansLoanIdResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; -import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdResponse; import org.apache.fineract.client.models.PutLoansLoanIdResponse; import org.apache.fineract.client.util.JSON; import org.apache.fineract.integrationtests.client.IntegrationTest; @@ -116,8 +115,17 @@ public class LoanTransactionHelper extends IntegrationTest { return this.getLoanId(loanApplicationJSON, this.requestSpec, this.responseSpec); } + public HashMap getLoanId(final String loanApplicationJSON, final String responseAttribute) { + return (HashMap) this.getLoanId(loanApplicationJSON, responseAttribute, this.requestSpec, this.responseSpec); + } + + public Object getLoanId(final String loanApplicationJSON, final String responseAttribute, RequestSpecification requestSpec, + ResponseSpecification responseSpec) { + return Utils.performServerPost(requestSpec, responseSpec, APPLY_LOAN_URL, loanApplicationJSON, responseAttribute); + } + public Integer getLoanId(final String loanApplicationJSON, RequestSpecification requestSpec, ResponseSpecification responseSpec) { - return Utils.performServerPost(requestSpec, responseSpec, APPLY_LOAN_URL, loanApplicationJSON, "loanId"); + return (Integer) getLoanId(loanApplicationJSON, "loanId", requestSpec, responseSpec); } public HashMap getGlimId(final String loanApplicationJSON) { @@ -258,12 +266,12 @@ public class LoanTransactionHelper extends IntegrationTest { return Utils.performServerGet(requestSpec, responseSpec, GET_REPAYMENTS_URL, "loanRepaymentScheduleInstallments"); } - public PostLoansLoanIdTransactionsTransactionIdResponse applyLoanTransactionCommand(final Integer loanId, final Integer transactionId, + public PostLoansLoanIdTransactionsResponse applyLoanTransactionCommand(final Integer loanId, final Integer transactionId, final String command, final String payload, final ResponseSpecification responseSpec) { final String LOAN_TRANSACTION_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/" + transactionId + "?command=" + command + "&" + Utils.TENANT_IDENTIFIER; final String response = Utils.performServerPost(requestSpec, responseSpec, LOAN_TRANSACTION_URL, payload, null); - return GSON.fromJson(response, PostLoansLoanIdTransactionsTransactionIdResponse.class); + return GSON.fromJson(response, PostLoansLoanIdTransactionsResponse.class); } public HashMap approveLoan(final String approvalDate, final Integer loanID) { @@ -305,6 +313,11 @@ public class LoanTransactionHelper extends IntegrationTest { getDisburseLoanAsJSON(date, null, netDisbursalAmount)); } + public HashMap disburseLoan(final String date, final Integer loanID, final String netDisbursalAmount, final String externalId) { + return (HashMap) performLoanTransaction(createLoanOperationURL(DISBURSE_LOAN_COMMAND, loanID), + getDisburseLoanAsJSON(date, null, netDisbursalAmount, externalId), ""); + } + public HashMap disburseLoanWithTransactionAmount(final String date, final Integer loanID, final String transactionAmount) { return performLoanTransaction(createLoanOperationURL(DISBURSE_LOAN_COMMAND, loanID), getDisburseLoanAsJSON(date, transactionAmount, null)); @@ -469,12 +482,179 @@ public class LoanTransactionHelper extends IntegrationTest { return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "repayment")); } + public HashMap undoWaiveLoanCharge(final Long loanId, final String transactionExternalId) { + log.info("--------------------------------- UNDO WAIVE CHARGES FOR LOAN --------------------------------"); + final String TRANSAC_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/external-id/" + transactionExternalId + "?" + + Utils.TENANT_IDENTIFIER; + return Utils.performServerPut(requestSpec, responseSpec, TRANSAC_URL, "", ""); + } + + public HashMap undoWaiveLoanCharge(final String loanExternalId, final Long transactionId) { + log.info("--------------------------------- UNDO WAIVE CHARGES FOR LOAN --------------------------------"); + final String TRANSAC_URL = "/fineract-provider/api/v1/loans/external-id/" + loanExternalId + "/transactions/" + transactionId + "?" + + Utils.TENANT_IDENTIFIER; + return Utils.performServerPut(requestSpec, responseSpec, TRANSAC_URL, "", ""); + } + + public HashMap undoWaiveLoanCharge(final String loanExternalId, final String transactionExternalId) { + log.info("--------------------------------- UNDO WAIVE CHARGES FOR LOAN --------------------------------"); + final String TRANSAC_URL = "/fineract-provider/api/v1/loans/external-id/" + loanExternalId + "/transactions/external-id/" + + transactionExternalId + "?" + Utils.TENANT_IDENTIFIER; + return Utils.performServerPut(requestSpec, responseSpec, TRANSAC_URL, "", ""); + } + + public PostLoansLoanIdChargesChargeIdResponse waiveLoanCharge(final Long loanId, final Long chargeId, + final PostLoansLoanIdChargesChargeIdRequest request) { + return ok(fineract().loanCharges.executeLoanCharge1(loanId, chargeId, request, "waive")); + } + + public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final String loanExternalId, + final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "repayment")); + } + + public PostLoansLoanIdTransactionsResponse makeMerchantIssuedRefund(final Long loanId, + final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "merchantIssuedRefund")); + } + + public PostLoansLoanIdTransactionsResponse makeMerchantIssuedRefund(final String loanExternalId, + final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "merchantIssuedRefund")); + } + + public PostLoansLoanIdTransactionsResponse makePayoutRefund(final Long loanId, final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "payoutRefund")); + } + + public PostLoansLoanIdTransactionsResponse makePayoutRefund(final String loanExternalId, + final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "payoutRefund")); + } + + public PostLoansLoanIdTransactionsResponse makeChargeRefund(final Long loanId, final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "chargeRefund")); + } + + public PostLoansLoanIdTransactionsResponse makeChargeRefund(final String loanExternalId, + final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "chargeRefund")); + } + + public PostLoansLoanIdTransactionsResponse makeGoodwillCredit(final Long loanId, final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "goodwillCredit")); + } + + public PostLoansLoanIdTransactionsResponse makeGoodwillCredit(final String loanExternalId, + final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "goodwillCredit")); + } + + public PostLoansLoanIdTransactionsResponse makeWaiveInterest(final Long loanId, final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "waiveinterest")); + } + + public PostLoansLoanIdTransactionsResponse makeWaiveInterest(final String loanExternalId, + final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "waiveinterest")); + } + + public PostLoansLoanIdTransactionsResponse makeWriteoff(final Long loanId, final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "writeoff")); + } + + public PostLoansLoanIdTransactionsResponse makeWriteoff(final String loanExternalId, final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "writeoff")); + } + + public PostLoansLoanIdTransactionsResponse makeUndoWriteoff(final Long loanId, final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "undowriteoff")); + } + + public PostLoansLoanIdTransactionsResponse makeUndoWriteoff(final String loanExternalId, + final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "undowriteoff")); + } + + public PostLoansLoanIdTransactionsResponse makeRecoveryPayment(final Long loanId, final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "recoverypayment")); + } + + public PostLoansLoanIdTransactionsResponse makeRecoveryPayment(final String loanExternalId, + final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "recoverypayment")); + } + + public PostLoansLoanIdTransactionsResponse makeRefundByCash(final Long loanId, final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "refundByCash")); + } + + public PostLoansLoanIdTransactionsResponse makeRefundByCash(final String loanExternalId, + final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "refundByCash")); + } + + public PostLoansLoanIdTransactionsResponse makeCreditBalanceRefund(final Long loanId, + final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "creditBalanceRefund")); + } + + public PostLoansLoanIdTransactionsResponse makeCreditBalanceRefund(final String loanExternalId, + final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "creditBalanceRefund")); + } + + public PostLoansLoanIdTransactionsResponse reverseLoanTransaction(final String loanExternalId, final Long transactionId, + final PostLoansLoanIdTransactionsTransactionIdRequest request) { + return ok(fineract().loanTransactions.adjustLoanTransaction2(loanExternalId, transactionId, request, "undo")); + } + + public PostLoansLoanIdTransactionsResponse reverseLoanTransaction(final String loanExternalId, final String transactionExternalId, + final PostLoansLoanIdTransactionsTransactionIdRequest request) { + return ok(fineract().loanTransactions.adjustLoanTransaction3(loanExternalId, transactionExternalId, request, "undo")); + } + + public PostLoansLoanIdTransactionsResponse reverseLoanTransaction(final Long loanId, final String transactionExternalId, + final PostLoansLoanIdTransactionsTransactionIdRequest request) { + return ok(fineract().loanTransactions.adjustLoanTransaction1(loanId, transactionExternalId, request, "undo")); + } + + public PostLoansLoanIdTransactionsResponse chargebackLoanTransaction(final String loanExternalId, final Long transactionId, + final PostLoansLoanIdTransactionsTransactionIdRequest request) { + return ok(fineract().loanTransactions.adjustLoanTransaction2(loanExternalId, transactionId, request, "chargeback")); + } + + public PostLoansLoanIdTransactionsResponse chargebackLoanTransaction(final String loanExternalId, final String transactionExternalId, + final PostLoansLoanIdTransactionsTransactionIdRequest request) { + return ok(fineract().loanTransactions.adjustLoanTransaction3(loanExternalId, transactionExternalId, request, "chargeback")); + } + + public PostLoansLoanIdTransactionsResponse chargebackLoanTransaction(final Long loanId, final String transactionExternalId, + final PostLoansLoanIdTransactionsTransactionIdRequest request) { + return ok(fineract().loanTransactions.adjustLoanTransaction1(loanId, transactionExternalId, request, "chargeback")); + } + + public PostLoansLoanIdTransactionsResponse adjustLoanTransaction(final String loanExternalId, final Long transactionId, + final PostLoansLoanIdTransactionsTransactionIdRequest request) { + return ok(fineract().loanTransactions.adjustLoanTransaction2(loanExternalId, transactionId, request, "adjust")); + } + + public PostLoansLoanIdTransactionsResponse adjustLoanTransaction(final String loanExternalId, final String transactionExternalId, + final PostLoansLoanIdTransactionsTransactionIdRequest request) { + return ok(fineract().loanTransactions.adjustLoanTransaction3(loanExternalId, transactionExternalId, request, "adjust")); + } + + public PostLoansLoanIdTransactionsResponse adjustLoanTransaction(final Long loanId, final String transactionExternalId, + final PostLoansLoanIdTransactionsTransactionIdRequest request) { + return ok(fineract().loanTransactions.adjustLoanTransaction1(loanId, transactionExternalId, request, "adjust")); + } + public PostLoansLoanIdTransactionsResponse reverseLoanTransaction(final Integer loanId, final Long transactionId, String date, ResponseSpecification responseSpec) { return postLoanTransaction(createLoanTransactionURL(UNDO, loanId, transactionId.intValue()), getUndoJsonBody(date), responseSpec); } - public PostLoansLoanIdTransactionsTransactionIdResponse reverseLoanTransaction(final Long loanId, final Long transactionId, + public PostLoansLoanIdTransactionsResponse reverseLoanTransaction(final Long loanId, final Long transactionId, final PostLoansLoanIdTransactionsTransactionIdRequest request) { return ok(fineract().loanTransactions.adjustLoanTransaction(loanId, transactionId, request, "undo")); } @@ -574,6 +754,13 @@ public class LoanTransactionHelper extends IntegrationTest { return (Integer) response.get("resourceId"); } + public HashMap payChargesForLoan(final Integer loanId, final Integer loanchargeId, final String json, final String responseAttribute) { + log.info("--------------------------------- WAIVE CHARGES FOR LOAN --------------------------------"); + final String CHARGES_URL = "/fineract-provider/api/v1/loans/" + loanId + "/charges/" + loanchargeId + "?command=pay&" + + Utils.TENANT_IDENTIFIER; + return Utils.performServerPost(requestSpec, responseSpec, CHARGES_URL, json, responseAttribute); + } + public ArrayList getLoanTransactionDetails(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, final Integer loanID) { final String URL = "/fineract-provider/api/v1/loans/" + loanID + "?associations=all&exclude=guarantors,futureSchedule&" @@ -616,6 +803,11 @@ public class LoanTransactionHelper extends IntegrationTest { private String getDisburseLoanAsJSON(final String actualDisbursementDate, final String transactionAmount, final String netDisbursalAmount) { + return getDisburseLoanAsJSON(actualDisbursementDate, transactionAmount, netDisbursalAmount, null); + } + + private String getDisburseLoanAsJSON(final String actualDisbursementDate, final String transactionAmount, + final String netDisbursalAmount, final String externalId) { final HashMap map = new HashMap<>(); map.put("locale", "en"); map.put("dateFormat", "dd MMMM yyyy"); @@ -627,6 +819,9 @@ public class LoanTransactionHelper extends IntegrationTest { if (transactionAmount != null) { map.put("transactionAmount", transactionAmount); } + if (externalId != null) { + map.put("externalId", externalId); + } log.info("Loan Application disburse request : {} ", map); return new Gson().toJson(map); } @@ -842,6 +1037,10 @@ public class LoanTransactionHelper extends IntegrationTest { } public static String getPayChargeJSON(final String date, final String installmentNumber) { + return getPayChargeJSON(date, installmentNumber, null); + } + + public static String getPayChargeJSON(final String date, final String installmentNumber, final String externalId) { final HashMap map = new HashMap<>(); map.put("locale", "en_GB"); map.put("dateFormat", "dd MMMM yyyy"); @@ -849,6 +1048,9 @@ public class LoanTransactionHelper extends IntegrationTest { if (installmentNumber != null) { map.put("installmentNumber", installmentNumber); } + if (externalId != null) { + map.put("externalId", externalId); + } String json = new Gson().toJson(map); log.info("{}", json); return json; @@ -1236,7 +1438,7 @@ public class LoanTransactionHelper extends IntegrationTest { final String payload = createChargebackPayload(amount, paymentTypeList.get(paymentTypeIdx).getId()); log.info("Loan Chargeback: {}", payload); - PostLoansLoanIdTransactionsTransactionIdResponse postLoansTransactionCommandResponse = applyLoanTransactionCommand(loanId, + PostLoansLoanIdTransactionsResponse postLoansTransactionCommandResponse = applyLoanTransactionCommand(loanId, transactionId.intValue(), "chargeback", payload, responseSpec); assertNotNull(postLoansTransactionCommandResponse);