diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferType.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferType.java index 8efb15e492f..536fad14c2d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferType.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferType.java @@ -27,7 +27,8 @@ public enum AccountTransferType { ACCOUNT_TRANSFER(1, "accountTransferType.account.transfer"), // LOAN_REPAYMENT(2, "accountTransferType.loan.repayment"), // CHARGE_PAYMENT(3, "accountTransferType.charge.payment"), // - INTEREST_TRANSFER(4, "accountTransferType.interest.transfer"); // + INTEREST_TRANSFER(4, "accountTransferType.interest.transfer"), // + LOAN_DOWN_PAYMENT(5, "accountTransferType.loan.downpayment"); // private final Integer value; private final String code; @@ -48,6 +49,9 @@ public static AccountTransferType fromInt(final Integer statusValue) { case 4: enumeration = AccountTransferType.INTEREST_TRANSFER; break; + case 5: + enumeration = AccountTransferType.LOAN_DOWN_PAYMENT; + break; } return enumeration; } @@ -84,4 +88,9 @@ public boolean isChargePayment() { public boolean isInterestTransfer() { return this.value.equals(AccountTransferType.INTEREST_TRANSFER.getValue()); } + + public boolean isLoanDownPayment() { + return this.value.equals(AccountTransferType.LOAN_DOWN_PAYMENT.getValue()); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransferEnumerations.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransferEnumerations.java index 14a3b091a79..99185797075 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransferEnumerations.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransferEnumerations.java @@ -107,6 +107,11 @@ public static EnumOptionData transferType(final AccountTransferType type) { optionData = new EnumOptionData(AccountTransferType.INTEREST_TRANSFER.getValue().longValue(), AccountTransferType.INTEREST_TRANSFER.getCode(), "Interest Transfer"); break; + case LOAN_DOWN_PAYMENT: + optionData = new EnumOptionData(AccountTransferType.LOAN_DOWN_PAYMENT.getValue().longValue(), + AccountTransferType.LOAN_DOWN_PAYMENT.getCode(), "Loan Down Payment"); + break; + } } return optionData; 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 ca2305c8de4..58dfc01bae3 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 @@ -329,6 +329,16 @@ public Long transferFunds(final AccountTransferDTO accountTransferDTO) { accountTransferDTO.getPaymentDetail(), null, externalId, accountTransferDTO.getToTransferType(), accountTransferDTO.getLoanInstallmentNumber()); + } else if (AccountTransferType.fromInt(accountTransferDTO.getTransferType()).isLoanDownPayment()) { + final boolean isRecoveryRepayment = false; + final Boolean isHolidayValidationDone = false; + final HolidayDetailDTO holidayDetailDto = null; + final String chargeRefundChargeType = null; + loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.DOWN_PAYMENT, toLoanAccount, + accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), + accountTransferDTO.getPaymentDetail(), null, externalId, isRecoveryRepayment, chargeRefundChargeType, + isAccountTransfer, holidayDetailDto, isHolidayValidationDone); + toLoanAccount = loanTransaction.getLoan(); } else { final boolean isRecoveryRepayment = false; final Boolean isHolidayValidationDone = false; 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 5bb617bbd98..b3a9798ca3b 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 @@ -59,6 +59,7 @@ 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.core.service.MathUtil; import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum; import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService; @@ -384,6 +385,8 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand Money amountBeforeAdjust = loan.getPrincipal(); boolean canDisburse = loan.canDisburse(actualDisbursementDate); ChangedTransactionDetail changedTransactionDetail = null; + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); if (canDisburse) { // Get netDisbursalAmount from disbursal screen field. @@ -458,7 +461,31 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand } loan.adjustNetDisbursalAmount(amountToDisburse.getAmount()); if (loan.isAutoRepaymentForDownPaymentEnabled()) { - loanDownPaymentHandlerService.handleDownPayment(scheduleGeneratorDTO, command, amountToDisburse, loan); + // updating linked savings account for auto down payment transaction for disbursement to savings account + if (isAccountTransfer && loan.shouldCreateStandingInstructionAtDisbursement()) { + final PortfolioAccountData linkedSavingsAccountData = this.accountAssociationsReadPlatformService + .retriveLoanLinkedAssociation(loanId); + final SavingsAccount fromSavingsAccount = null; + final boolean isRegularTransaction = true; + final boolean isExceptionForBalanceCheck = false; + + BigDecimal disbursedAmountPercentageForDownPayment = loan.getLoanRepaymentScheduleDetail() + .getDisbursedAmountPercentageForDownPayment(); + Money downPaymentMoney = Money.of(loan.getCurrency(), + MathUtil.percentageOf(amountToDisburse.getAmount(), disbursedAmountPercentageForDownPayment, 19)); + + final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(actualDisbursementDate, + downPaymentMoney.getAmount(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, + linkedSavingsAccountData.getId(), loan.getId(), + "To loan " + loan.getAccountNumber() + " from savings " + linkedSavingsAccountData.getAccountNo() + + " Standing instruction transfer ", + locale, fmt, null, null, LoanTransactionType.DOWN_PAYMENT.getValue(), null, null, + AccountTransferType.LOAN_DOWN_PAYMENT.getValue(), null, null, ExternalId.empty(), null, null, + fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); + this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); + } else { + loanDownPaymentHandlerService.handleDownPayment(scheduleGeneratorDTO, command, amountToDisburse, loan); + } } } if (!changes.isEmpty()) { @@ -498,9 +525,6 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(savedLoanTransaction)); } } - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); for (final Map.Entry entrySet : disBuLoanCharges.entrySet()) { final PortfolioAccountData savingAccountData = this.accountAssociationsReadPlatformService.retriveLoanLinkedAssociation(loanId); final SavingsAccount fromSavingsAccount = null; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountDisbursementToSavingsWithAutoDownPaymentTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountDisbursementToSavingsWithAutoDownPaymentTest.java new file mode 100644 index 00000000000..e95f6efd38a --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountDisbursementToSavingsWithAutoDownPaymentTest.java @@ -0,0 +1,255 @@ +/** + * 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 static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.apache.fineract.accounting.common.AccountingConstants; +import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; +import org.apache.fineract.client.models.GetSavingsAccountTransactionsPageItem; +import org.apache.fineract.client.models.PostLoanProductsRequest; +import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdRequest; +import org.apache.fineract.client.models.PostLoansLoanIdResponse; +import org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.CommonConstants; +import org.apache.fineract.integrationtests.common.accounting.FinancialActivityAccountHelper; +import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; +import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper; +import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class LoanAccountDisbursementToSavingsWithAutoDownPaymentTest extends BaseLoanIntegrationTest { + + public static final BigDecimal DOWN_PAYMENT_PERCENTAGE = new BigDecimal(25); + + @Test + public void loanDisbursementToSavingsWithAutoDownPaymentAndStandingInstructionsTest() { + runAt("01 March 2023", () -> { + + // loan external Id + String loanExternalIdStr = UUID.randomUUID().toString(); + + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + // Create Loan Product + Long loanProductId = createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(); + + SavingsAccountHelper savingsAccountHelper = new SavingsAccountHelper(requestSpec, responseSpec); + + // Create approve and activate savings account + Integer savingsAccountId = createApproveActivateSavingsAccountDailyPosting(clientId.intValue(), "01 March 2023", + savingsAccountHelper); + + // create Financial Activity Mapping for Liability Transfer + mapLiabilityTransferFinancialActivity(loanProductId); + + // Apply and Approve Loan + Long loanId = createLoanWithLinkedAccountAndStandingInstructions(clientId.intValue(), loanProductId, savingsAccountId, + loanExternalIdStr); + + // disburse to savings + PostLoansLoanIdResponse responseLoanDisburseToSavings = loanTransactionHelper.disburseToSavingsLoan(loanExternalIdStr, + new PostLoansLoanIdRequest().actualDisbursementDate("01 March 2023").transactionAmount(new BigDecimal("1000")) + .locale("en").dateFormat("dd MMMM yyyy")); + + assertEquals(loanExternalIdStr, responseLoanDisburseToSavings.getResourceExternalId()); + + // verify repayment schedule + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 March 2023"), // + installment(250.0, true, "01 March 2023"), // + installment(250.0, false, "16 March 2023"), // + installment(250.0, false, "31 March 2023"), // + installment(250.0, false, "15 April 2023")// + ); + + // verify Disbursement Transaction is account transfer + verifyTransactionIsAccountTransfer(LocalDate.of(2023, 3, 1), 1000.0f, loanId.intValue(), "disbursement"); + + // verify Down payment Transaction is account transfer + verifyTransactionIsAccountTransfer(LocalDate.of(2023, 3, 1), 250.0f, loanId.intValue(), "downPayment"); + + // verify savings transactions + verifySavingsTransactions(savingsAccountId, savingsAccountHelper); + + }); + } + + private void verifySavingsTransactions(final Integer savingsId, final SavingsAccountHelper savingsAccountHelper) { + Map queryParams = new HashMap<>(); + SavingsAccountTransactionsSearchResponse transactionsResponse = savingsAccountHelper.searchSavingsTransactions(savingsId, + queryParams); + + Assertions.assertNotNull(transactionsResponse); + assertEquals(2, transactionsResponse.getTotal()); + Assertions.assertNotNull(transactionsResponse.getContent()); + List pageItemsList = List.copyOf(transactionsResponse.getContent()); + assertEquals(2, pageItemsList.size()); + + // check withdrawal + GetSavingsAccountTransactionsPageItem withDrawalTransaction = pageItemsList.get(0); + assertEquals("savingsAccountTransactionType.withdrawal", withDrawalTransaction.getTransactionType().getCode()); + assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(250), withDrawalTransaction.getAmount())); + assertEquals("DEBIT", withDrawalTransaction.getEntryType().getValue()); + assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(750), withDrawalTransaction.getRunningBalance())); + + // check deposit + GetSavingsAccountTransactionsPageItem depositTransaction = pageItemsList.get(1); + assertEquals("savingsAccountTransactionType.deposit", depositTransaction.getTransactionType().getCode()); + assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(1000), depositTransaction.getAmount())); + assertEquals("CREDIT", depositTransaction.getEntryType().getValue()); + assertTrue(MathUtil.isEqualTo(BigDecimal.valueOf(1000), depositTransaction.getRunningBalance())); + + } + + private void mapLiabilityTransferFinancialActivity(Long loanProductId) { + FinancialActivityAccountHelper financialActivityAccountHelper = new FinancialActivityAccountHelper(requestSpec); + GetLoanProductsProductIdResponse getLoanProductsProductIdResponse = loanProductHelper.retrieveLoanProductById(loanProductId); + Integer financialActivityAccountId = (Integer) financialActivityAccountHelper.createFinancialActivityAccount( + AccountingConstants.FinancialActivity.LIABILITY_TRANSFER.getValue(), + getLoanProductsProductIdResponse.getAccountingMappings().getFundSourceAccount().getId().intValue(), responseSpec, + CommonConstants.RESPONSE_RESOURCE_ID); + assertNotNull(financialActivityAccountId); + } + + private Long createLoanWithLinkedAccountAndStandingInstructions(final Integer clientID, final Long loanProductID, + final Integer savingsId, final String externalId) { + + String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("45") + .withLoanTermFrequencyAsDays().withNumberOfRepayments("3").withRepaymentEveryAfter("15").withRepaymentFrequencyTypeAsDays() + .withInterestRatePerPeriod("0").withInterestTypeAsFlatBalance().withAmortizationTypeAsEqualPrincipalPayments() + .withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withExpectedDisbursementDate("01 March 2023") + .withSubmittedOnDate("01 March 2023").withLoanType("individual").withExternalId(externalId) + .withCreateStandingInstructionAtDisbursement().build(clientID.toString(), loanProductID.toString(), savingsId.toString()); + + final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON); + loanTransactionHelper.approveLoan("01 March 2023", "1000", loanId, null); + return loanId.longValue(); + } + + private Integer createApproveActivateSavingsAccountDailyPosting(final Integer clientID, final String startDate, + final SavingsAccountHelper savingsAccountHelper) { + final Integer savingsProductID = createSavingsProductDailyPosting(); + assertNotNull(savingsProductID); + return savingsAccountHelper.createApproveActivateSavingsAccount(clientID, savingsProductID, startDate); + } + + private Integer createSavingsProductDailyPosting() { + SavingsProductHelper savingsProductHelper = new SavingsProductHelper(); + final String savingsProductJSON = savingsProductHelper.withInterestCompoundingPeriodTypeAsDaily() + .withInterestPostingPeriodTypeAsMonthly().withInterestCalculationPeriodTypeAsDailyBalance().build(); + return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); + } + + private Long createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment() { + boolean multiDisburseEnabled = true; + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct(); + product.setMultiDisburseLoan(multiDisburseEnabled); + product.setNumberOfRepayments(3); + product.setRepaymentEvery(15); + + if (!multiDisburseEnabled) { + product.disallowExpectedDisbursements(null); + product.setAllowApprovedDisbursedAmountsOverApplied(null); + product.overAppliedCalculationType(null); + product.overAppliedNumber(null); + } + + product.setEnableDownPayment(true); + product.setDisbursedAmountPercentageForDownPayment(DOWN_PAYMENT_PERCENTAGE); + product.setEnableAutoRepaymentForDownPayment(true); + + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + GetLoanProductsProductIdResponse getLoanProductsProductIdResponse = loanProductHelper + .retrieveLoanProductById(loanProductResponse.getResourceId()); + assertNotNull(getLoanProductsProductIdResponse); + return loanProductResponse.getResourceId(); + + } + + private void verifyTransactionIsAccountTransfer(final LocalDate transactionDate, final Float transactionAmount, final Integer loanID, + final String transactionOfType) { + ArrayList transactions = (ArrayList) loanTransactionHelper.getLoanTransactions(requestSpec, responseSpec, loanID); + boolean isTransactionFound = false; + for (int i = 0; i < transactions.size(); i++) { + HashMap transactionType = (HashMap) transactions.get(i).get("type"); + boolean isTransaction = (Boolean) transactionType.get(transactionOfType); + + if (isTransaction) { + ArrayList transactionDateAsArray = (ArrayList) transactions.get(i).get("date"); + LocalDate transactionEntryDate = LocalDate.of(transactionDateAsArray.get(0), transactionDateAsArray.get(1), + transactionDateAsArray.get(2)); + + if (transactionDate.isEqual(transactionEntryDate)) { + isTransactionFound = true; + assertEquals(transactionAmount, Float.valueOf(String.valueOf(transactions.get(i).get("amount"))), + "Mismatch in transaction amounts"); + + // verify transfer details + assertNotNull(transactions.get(i).get("transfer")); + + final HashMap actualTransferMap = (HashMap) transactions.get(i).get("transfer"); + + assertEquals(transactionAmount, Float.valueOf(String.valueOf(actualTransferMap.get("transferAmount")))); + + ArrayList transferDate = (ArrayList) actualTransferMap.get("transferDate"); + + LocalDate dateOfTransfer = LocalDate.of(transferDate.get(0), transferDate.get(1), transferDate.get(2)); + assertTrue(transactionDate.isEqual(dateOfTransfer)); + + break; + } + } + } + + assertTrue(isTransactionFound, "No Transaction entries are posted"); + + } + + /** + * Delete the Financial activities + */ + @AfterEach + public void tearDown() { + FinancialActivityAccountHelper financialActivityAccountHelper = new FinancialActivityAccountHelper(requestSpec); + List financialActivities = financialActivityAccountHelper.getAllFinancialActivityAccounts(responseSpec); + for (HashMap financialActivity : financialActivities) { + Integer financialActivityAccountId = (Integer) financialActivity.get("id"); + Integer deletedFinancialActivityAccountId = financialActivityAccountHelper + .deleteFinancialActivityAccount(financialActivityAccountId, responseSpec, CommonConstants.RESPONSE_RESOURCE_ID); + Assertions.assertNotNull(deletedFinancialActivityAccountId); + Assertions.assertEquals(financialActivityAccountId, deletedFinancialActivityAccountId); + } + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanApplicationTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanApplicationTestBuilder.java index 840cb9fd23f..c6e49ab9b1e 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanApplicationTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanApplicationTestBuilder.java @@ -84,6 +84,7 @@ public class LoanApplicationTestBuilder { private String interestChargedFromDate; private String linkAccountId; private String inArrearsTolerance; + private boolean createStandingInstructionAtDisbursement = false; public String build(final String clientID, final String groupID, final String loanProductId, final String savingsID) { final HashMap map = new HashMap<>(); @@ -203,6 +204,10 @@ private String build(final HashMap map, final String loanProduct if (datatables != null) { map.put("datatables", this.datatables); } + + if (createStandingInstructionAtDisbursement == true) { + map.put("createStandingInstructionAtDisbursement", true); + } LOG.info("Loan Application request : {} ", map); return new Gson().toJson(map); } @@ -430,4 +435,9 @@ public LoanApplicationTestBuilder withInArrearsTolerance(String amount) { this.inArrearsTolerance = amount; return this; } + + public LoanApplicationTestBuilder withCreateStandingInstructionAtDisbursement() { + this.createStandingInstructionAtDisbursement = true; + return this; + } }