diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index ac4955d79..8d5ddd298 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -1139,6 +1139,15 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder disburseWithoutAutoDownPayment(final Long loanId) { + this.actionName = "DISBURSEWITHOUTAUTODOWNPAYMENT"; + this.entityName = "LOAN"; + this.entityId = loanId; + this.loanId = loanId; + this.href = "/loans/" + loanId; + return this; + } + public CommandWrapperBuilder undoLoanApplicationApproval(final Long loanId) { this.actionName = "APPROVALUNDO"; this.entityName = "LOAN"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index 53d094e9a..006149d7d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java @@ -1166,6 +1166,8 @@ public class LoansApiResource { commandRequest = builder.disburseLoanApplication(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "disburseToSavings")) { commandRequest = builder.disburseLoanToSavingsApplication(resolvedLoanId).build(); + } else if (CommandParameterUtil.is(commandParam, "disburseWithoutAutoDownPayment")) { + commandRequest = builder.disburseWithoutAutoDownPayment(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "undoapproval")) { commandRequest = builder.undoLoanApplicationApproval(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "undodisbursal")) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/DisburseLoanWithoutAutoPaymentHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/DisburseLoanWithoutAutoPaymentHandler.java new file mode 100644 index 000000000..54a09e74a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/DisburseLoanWithoutAutoPaymentHandler.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.portfolio.loanaccount.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.DataIntegrityErrorHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "LOAN", action = "DISBURSEWITHOUTAUTODOWNPAYMENT") +public class DisburseLoanWithoutAutoPaymentHandler implements NewCommandSourceHandler { + + private final LoanWritePlatformService writePlatformService; + private final DataIntegrityErrorHandler dataIntegrityErrorHandler; + + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + try { + return writePlatformService.disburseLoan(command.entityId(), command, false, true); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + dataIntegrityErrorHandler.handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.disbursement", + "Disbursement"); + return CommandProcessingResult.empty(); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java index 4b09cd2b9..c3f3e4166 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java @@ -38,6 +38,8 @@ public interface LoanWritePlatformService { CommandProcessingResult disburseLoan(Long loanId, JsonCommand command, Boolean isAccountTransfer); + CommandProcessingResult disburseLoan(Long loanId, JsonCommand command, Boolean isAccountTransfer, Boolean isWithoutAutoPayment); + Map bulkLoanDisbursal(JsonCommand command, CollectionSheetBulkDisbursalCommand bulkDisbursalCommand, Boolean isAccountTransfer); 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 b8dccde4f..b38b22420 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 @@ -273,9 +273,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf return result; } + @Override + public CommandProcessingResult disburseLoan(Long loanId, JsonCommand command, Boolean isAccountTransfer) { + return disburseLoan(loanId, command, isAccountTransfer, false); + } + @Transactional @Override - public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand command, Boolean isAccountTransfer) { + public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand command, Boolean isAccountTransfer, + Boolean isWithoutAutoPayment) { final AppUser currentUser = getAppUserIfPresent(); @@ -469,7 +475,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf // Trigger transaction replayed event replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } - if (loan.isAutoRepaymentForDownPaymentEnabled()) { + if (loan.isAutoRepaymentForDownPaymentEnabled() && !isWithoutAutoPayment) { // updating linked savings account for auto down payment transaction for disbursement to savings account if (isAccountTransfer && loan.shouldCreateStandingInstructionAtDisbursement()) { final PortfolioAccountData linkedSavingsAccountData = this.accountAssociationsReadPlatformService 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 986954914..442d18553 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 @@ -158,4 +158,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0139_add_disburse_without_auto_payment_command.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0139_add_disburse_without_auto_payment_command.xml new file mode 100644 index 000000000..630272ac3 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0139_add_disburse_without_auto_payment_command.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java index 1b8b497a4..253fc7b69 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java @@ -1611,6 +1611,58 @@ public class LoanRepaymentScheduleWithDownPaymentTest extends BaseLoanIntegratio }); } + @Test + public void loanRepaymentScheduleWithSimpleDisbursementAndWithoutAutoPayment() { + String loanExternalIdStr = UUID.randomUUID().toString(); + + final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec); + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketId); + + Boolean enableDownPayment = true; + BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(25); + Boolean enableAutoRepaymentForDownPayment = true; + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + + Integer loanProductId = createLoanProductWithDownPaymentConfiguration(loanTransactionHelper, delinquencyBucketId, enableDownPayment, + "25", enableAutoRepaymentForDownPayment, false); + + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId); + assertNotNull(getLoanProductsProductResponse); + assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); + assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() + .compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); + + final Integer loanId = createAndApproveLoanAccount(clientId, loanProductId.longValue(), loanExternalIdStr, "1", "0"); + loanTransactionHelper.disburseLoanWithTransactionAmountAndWithoutAutoPayment("03 September 2022", loanId, "1000"); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + GetLoansLoanIdSummary summary = loanDetails.getSummary(); + + assertNotNull(loanDetails); + assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); + assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); + + List periods = loanDetails.getRepaymentSchedule().getPeriods(); + Double expectedDownPaymentAmount = 250.00; + LocalDate expectedDownPaymentDueDate = LocalDate.of(2022, 9, 3); + Double expectedRepaymentAmount = 750.00; + LocalDate expectedRepaymentDueDate = LocalDate.of(2022, 10, 3); + Double expectedTotalOutstandingAmount = 1000.00; + Double expectedTotalRepaymentTransactionAmount = 0.00; + + assertTrue(periods.stream() // + .anyMatch(period -> expectedDownPaymentAmount.equals(period.getTotalOutstandingForPeriod()) // + && expectedDownPaymentDueDate.equals(period.getDueDate()))); + assertEquals(expectedTotalOutstandingAmount, summary.getTotalOutstanding()); + assertEquals(expectedTotalRepaymentTransactionAmount, summary.getTotalRepaymentTransaction()); + assertTrue(periods.stream().anyMatch(period -> expectedRepaymentAmount.equals(period.getTotalOutstandingForPeriod()) + && expectedRepaymentDueDate.equals(period.getDueDate()))); + } + private void checkNoDownPaymentTransaction(final Integer loanID) { ArrayList transactions = (ArrayList) loanTransactionHelper.getLoanTransactions(requestSpec, responseSpec, loanID); boolean isTransactionFound = false; @@ -1735,7 +1787,7 @@ public class LoanRepaymentScheduleWithDownPaymentTest extends BaseLoanIntegratio return loanProductId; } - private Integer createApproveAndDisburseLoanAccount(final Integer clientID, final Long loanProductID, final String externalId, + private Integer createAndApproveLoanAccount(final Integer clientID, final Long loanProductID, final String externalId, final String numberOfRepayments, final String interestRate) { String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency(numberOfRepayments) @@ -1747,6 +1799,13 @@ public class LoanRepaymentScheduleWithDownPaymentTest extends BaseLoanIntegratio final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON); loanTransactionHelper.approveLoan("02 September 2022", "1000", loanId, null); + return loanId; + } + + private Integer createApproveAndDisburseLoanAccount(final Integer clientID, final Long loanProductID, final String externalId, + final String numberOfRepayments, final String interestRate) { + + Integer loanId = createAndApproveLoanAccount(clientID, loanProductID, externalId, numberOfRepayments, interestRate); loanTransactionHelper.disburseLoanWithTransactionAmount("03 September 2022", loanId, "1000"); return loanId; } 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 232cc48ac..a81c20804 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 @@ -108,6 +108,7 @@ public class LoanTransactionHelper extends IntegrationTest { private static final String UNDO_APPROVAL_LOAN_COMMAND = "undoApproval"; private static final String DISBURSE_LOAN_COMMAND = "disburse"; private static final String DISBURSE_LOAN_TO_SAVINGS_COMMAND = "disburseToSavings"; + private static final String DISBURSE_LOAN_WITHOUT_AUTO_PAYMENT_COMMAND = "disburseWithoutAutoDownPayment"; private static final String UNDO_DISBURSE_LOAN_COMMAND = "undoDisbursal"; private static final String REJECT_LOAN_COMMAND = "reject"; private static final String UNDO_LAST_DISBURSE_LOAN_COMMAND = "undolastdisbursal"; @@ -423,6 +424,12 @@ public class LoanTransactionHelper extends IntegrationTest { getDisburseLoanAsJSON(date, transactionAmount, null)); } + public HashMap disburseLoanWithTransactionAmountAndWithoutAutoPayment(final String date, final Integer loanID, + final String transactionAmount) { + return performLoanTransaction(createLoanOperationURL(DISBURSE_LOAN_WITHOUT_AUTO_PAYMENT_COMMAND, loanID), + getDisburseLoanAsJSON(date, transactionAmount, null)); + } + public HashMap disburseLoanWithPostDatedChecks(final String date, final Integer loanId, final BigDecimal transactionAmount, final List postDatedChecks) { return performLoanTransaction(createLoanOperationURL(DISBURSE_LOAN_COMMAND, loanId),