FINERACT-1958
- reverse handling of disbursement with down payment
This commit is contained in:
+39
-2
@@ -3044,7 +3044,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
? Boolean.FALSE
|
? Boolean.FALSE
|
||||||
: Boolean.TRUE;
|
: Boolean.TRUE;
|
||||||
this.loanRepaymentScheduleDetail.setPrincipal(this.approvedPrincipal);
|
this.loanRepaymentScheduleDetail.setPrincipal(this.approvedPrincipal);
|
||||||
if (this.loanProduct.isMultiDisburseLoan()) {
|
// Remove All the Disbursement Details If the Loan Product is disabled and exists one
|
||||||
|
if (this.loanProduct().isDisallowExpectedDisbursements() && !getDisbursementDetails().isEmpty()) {
|
||||||
|
for (LoanDisbursementDetails disbursementDetail : getAllDisbursementDetails()) {
|
||||||
|
disbursementDetail.reverse();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
for (final LoanDisbursementDetails details : getDisbursementDetails()) {
|
for (final LoanDisbursementDetails details : getDisbursementDetails()) {
|
||||||
details.updateActualDisbursementDate(null);
|
details.updateActualDisbursementDate(null);
|
||||||
}
|
}
|
||||||
@@ -5535,6 +5540,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ChangedTransactionDetail recalculateScheduleFromLastTransaction(final ScheduleGeneratorDTO generatorDTO) {
|
||||||
|
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||||
|
regenerateRepaymentScheduleWithInterestRecalculation(generatorDTO);
|
||||||
|
} else {
|
||||||
|
regenerateRepaymentSchedule(generatorDTO);
|
||||||
|
}
|
||||||
|
return processTransactions();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public ChangedTransactionDetail handleRegenerateRepaymentScheduleWithInterestRecalculation(final ScheduleGeneratorDTO generatorDTO) {
|
public ChangedTransactionDetail handleRegenerateRepaymentScheduleWithInterestRecalculation(final ScheduleGeneratorDTO generatorDTO) {
|
||||||
regenerateRepaymentScheduleWithInterestRecalculation(generatorDTO);
|
regenerateRepaymentScheduleWithInterestRecalculation(generatorDTO);
|
||||||
return processTransactions();
|
return processTransactions();
|
||||||
@@ -6505,7 +6520,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
reverseExistingTransactionsTillLastDisbursal(lastDisbursalTransaction);
|
reverseExistingTransactionsTillLastDisbursal(lastDisbursalTransaction);
|
||||||
loan.recalculateScheduleFromLastTransaction(scheduleGeneratorDTO, existingTransactionIds, existingReversedTransactionIds);
|
loan.recalculateScheduleFromLastTransaction(scheduleGeneratorDTO);
|
||||||
actualChanges.put("undolastdisbursal", "true");
|
actualChanges.put("undolastdisbursal", "true");
|
||||||
actualChanges.put("disbursedAmount", this.getDisbursedAmount());
|
actualChanges.put("disbursedAmount", this.getDisbursedAmount());
|
||||||
updateLoanSummaryDerivedFields();
|
updateLoanSummaryDerivedFields();
|
||||||
@@ -6529,6 +6544,28 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
transaction.reverse();
|
transaction.reverse();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isAutoRepaymentForDownPaymentEnabled()) {
|
||||||
|
// identify down-payment amount for the transaction
|
||||||
|
BigDecimal disbursedAmountPercentageForDownPayment = this.loanRepaymentScheduleDetail
|
||||||
|
.getDisbursedAmountPercentageForDownPayment();
|
||||||
|
Money downPaymentMoney = Money.of(getCurrency(),
|
||||||
|
MathUtil.percentageOf(lastDisbursalTransaction.getAmount(), disbursedAmountPercentageForDownPayment, 19));
|
||||||
|
|
||||||
|
// find the matching down-payment transaction based on date, amount and it also must have downpayment
|
||||||
|
// installment linked
|
||||||
|
Optional<LoanTransaction> downPaymentTransaction = this.loanTransactions.stream()
|
||||||
|
.filter(tr -> tr.getTransactionDate().equals(lastDisbursalTransaction.getTransactionDate())
|
||||||
|
&& hasAnInstallmentWithDownPayment(tr, downPaymentMoney.getAmount()))
|
||||||
|
.max(Comparator.comparing(LoanTransaction::getId));
|
||||||
|
|
||||||
|
// reverse the down-payment transaction
|
||||||
|
downPaymentTransaction.ifPresent(LoanTransaction::reverse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasAnInstallmentWithDownPayment(LoanTransaction tr, BigDecimal amount) {
|
||||||
|
return tr.getAmount().compareTo(amount) == 0 && tr.getLoanTransactionToRepaymentScheduleMappings().stream()
|
||||||
|
.anyMatch(mapping -> mapping.getLoanRepaymentScheduleInstallment().isDownPayment());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateLoanToLastDisbursalState(LoanDisbursementDetails disbursementDetail) {
|
private void updateLoanToLastDisbursalState(LoanDisbursementDetails disbursementDetail) {
|
||||||
|
|||||||
+1
-1
@@ -910,7 +910,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Boolean isAllowTypeTransactionAtTheTimeOfLastUndo() {
|
public Boolean isAllowTypeTransactionAtTheTimeOfLastUndo() {
|
||||||
return isDisbursement() || isAccrual() || isRepaymentAtDisbursement();
|
return isDisbursement() || isAccrual() || isRepaymentAtDisbursement() || isRepayment();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAccrualTransaction() {
|
public boolean isAccrualTransaction() {
|
||||||
|
|||||||
-10
@@ -869,16 +869,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
|
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
|
||||||
existingReversedTransactionIds, isAccountTransfer);
|
existingReversedTransactionIds, isAccountTransfer);
|
||||||
journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
|
journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
|
||||||
|
|
||||||
// Remove All the Disbursement Details If the Loan Product is disabled and exists one
|
|
||||||
if (loan.loanProduct().isDisallowExpectedDisbursements() && !loan.getDisbursementDetails().isEmpty()) {
|
|
||||||
List<LoanDisbursementDetails> reversedDisbursementDetails = new ArrayList<>();
|
|
||||||
for (LoanDisbursementDetails disbursementDetail : loan.getAllDisbursementDetails()) {
|
|
||||||
disbursementDetail.reverse();
|
|
||||||
reversedDisbursementDetails.add(disbursementDetail);
|
|
||||||
}
|
|
||||||
this.loanDisbursementDetailsRepository.saveAllAndFlush(reversedDisbursementDetails);
|
|
||||||
}
|
|
||||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoDisbursalBusinessEvent(loan));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoDisbursalBusinessEvent(loan));
|
||||||
}
|
}
|
||||||
|
|||||||
+338
@@ -0,0 +1,338 @@
|
|||||||
|
/**
|
||||||
|
* 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 java.lang.Boolean.FALSE;
|
||||||
|
import static java.lang.Boolean.TRUE;
|
||||||
|
import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
|
||||||
|
import static org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import io.restassured.builder.RequestSpecBuilder;
|
||||||
|
import io.restassured.builder.ResponseSpecBuilder;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import io.restassured.specification.RequestSpecification;
|
||||||
|
import io.restassured.specification.ResponseSpecification;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
import org.apache.fineract.client.models.AllowAttributeOverrides;
|
||||||
|
import org.apache.fineract.client.models.BusinessDateRequest;
|
||||||
|
import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
|
||||||
|
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||||
|
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||||
|
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
|
||||||
|
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
|
||||||
|
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
|
||||||
|
import org.apache.fineract.client.models.PostLoansRequest;
|
||||||
|
import org.apache.fineract.client.models.PostLoansResponse;
|
||||||
|
import org.apache.fineract.client.util.CallFailedRuntimeException;
|
||||||
|
import org.apache.fineract.integrationtests.common.BusinessDateHelper;
|
||||||
|
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.accounting.JournalEntryHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.loans.LoanProductHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
|
||||||
|
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
|
||||||
|
public abstract class BaseLoanIntegrationTest {
|
||||||
|
|
||||||
|
static {
|
||||||
|
Utils.initializeRESTAssured();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static final String DATETIME_PATTERN = "dd MMMM yyyy";
|
||||||
|
|
||||||
|
protected final ResponseSpecification requestSpec = createResponseSpecification(200);
|
||||||
|
protected final RequestSpecification responseSpec = createRequestSpecification();
|
||||||
|
|
||||||
|
protected final AccountHelper accountHelper = new AccountHelper(responseSpec, requestSpec);
|
||||||
|
protected final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(responseSpec, requestSpec);
|
||||||
|
protected final LoanProductHelper loanProductHelper = new LoanProductHelper();
|
||||||
|
protected JournalEntryHelper journalEntryHelper = new JournalEntryHelper(responseSpec, requestSpec);
|
||||||
|
protected BusinessDateHelper businessDateHelper = new BusinessDateHelper();
|
||||||
|
|
||||||
|
// asset
|
||||||
|
protected final Account loansReceivableAccount = accountHelper.createAssetAccount();
|
||||||
|
protected final Account interestFeeReceivableAccount = accountHelper.createAssetAccount();
|
||||||
|
protected final Account suspenseAccount = accountHelper.createAssetAccount();
|
||||||
|
// liability
|
||||||
|
protected final Account suspenseClearingAccount = accountHelper.createLiabilityAccount();
|
||||||
|
protected final Account overpaymentAccount = accountHelper.createLiabilityAccount();
|
||||||
|
// income
|
||||||
|
protected final Account interestIncomeAccount = accountHelper.createIncomeAccount();
|
||||||
|
protected final Account feeIncomeAccount = accountHelper.createIncomeAccount();
|
||||||
|
protected final Account feeChargeOffAccount = accountHelper.createIncomeAccount();
|
||||||
|
protected final Account recoveriesAccount = accountHelper.createIncomeAccount();
|
||||||
|
protected final Account interestIncomeChargeOffAccount = accountHelper.createIncomeAccount();
|
||||||
|
// expense
|
||||||
|
protected final Account creditLossBadDebtAccount = accountHelper.createExpenseAccount();
|
||||||
|
protected final Account creditLossBadDebtFraudAccount = accountHelper.createExpenseAccount();
|
||||||
|
protected final Account writtenOffAccount = accountHelper.createExpenseAccount();
|
||||||
|
protected final Account goodwillExpenseAccount = accountHelper.createExpenseAccount();
|
||||||
|
|
||||||
|
// Loan product with proper accounting setup
|
||||||
|
protected PostLoanProductsRequest createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() {
|
||||||
|
return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6))//
|
||||||
|
.shortName(Utils.uniqueRandomStringGenerator("", 4))//
|
||||||
|
.description("Loan Product Description")//
|
||||||
|
.includeInBorrowerCycle(false)//
|
||||||
|
.currencyCode("USD")//
|
||||||
|
.digitsAfterDecimal(2)//
|
||||||
|
.inMultiplesOf(0)//
|
||||||
|
.installmentAmountInMultiplesOf(1)//
|
||||||
|
.useBorrowerCycle(false)//
|
||||||
|
.minPrincipal(100.0)//
|
||||||
|
.principal(1000.0)//
|
||||||
|
.maxPrincipal(10000.0)//
|
||||||
|
.minNumberOfRepayments(1)//
|
||||||
|
.numberOfRepayments(1)//
|
||||||
|
.maxNumberOfRepayments(30)//
|
||||||
|
.isLinkedToFloatingInterestRates(false)//
|
||||||
|
.minInterestRatePerPeriod((double) 0)//
|
||||||
|
.interestRatePerPeriod((double) 0)//
|
||||||
|
.maxInterestRatePerPeriod((double) 0)//
|
||||||
|
.interestRateFrequencyType(2)//
|
||||||
|
.repaymentEvery(30)//
|
||||||
|
.repaymentFrequencyType(0)//
|
||||||
|
.amortizationType(1)//
|
||||||
|
.interestType(0)//
|
||||||
|
.isEqualAmortization(false)//
|
||||||
|
.interestCalculationPeriodType(1)//
|
||||||
|
.transactionProcessingStrategyCode(
|
||||||
|
LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY)//
|
||||||
|
.daysInYearType(1)//
|
||||||
|
.daysInMonthType(1)//
|
||||||
|
.canDefineInstallmentAmount(true)//
|
||||||
|
.graceOnArrearsAgeing(3)//
|
||||||
|
.overdueDaysForNPA(179)//
|
||||||
|
.accountMovesOutOfNPAOnlyOnArrearsCompletion(false)//
|
||||||
|
.principalThresholdForLastInstallment(50)//
|
||||||
|
.allowVariableInstallments(false)//
|
||||||
|
.canUseForTopup(false)//
|
||||||
|
.isInterestRecalculationEnabled(false)//
|
||||||
|
.holdGuaranteeFunds(false)//
|
||||||
|
.multiDisburseLoan(true)//
|
||||||
|
.allowAttributeOverrides(new AllowAttributeOverrides()//
|
||||||
|
.amortizationType(true)//
|
||||||
|
.interestType(true)//
|
||||||
|
.transactionProcessingStrategyCode(true)//
|
||||||
|
.interestCalculationPeriodType(true)//
|
||||||
|
.inArrearsTolerance(true)//
|
||||||
|
.repaymentEvery(true)//
|
||||||
|
.graceOnPrincipalAndInterestPayment(true)//
|
||||||
|
.graceOnArrearsAgeing(true))//
|
||||||
|
.allowPartialPeriodInterestCalcualtion(true)//
|
||||||
|
.maxTrancheCount(10)//
|
||||||
|
.outstandingLoanBalance(10000.0)//
|
||||||
|
.charges(Collections.emptyList())//
|
||||||
|
.accountingRule(3)//
|
||||||
|
.fundSourceAccountId(suspenseClearingAccount.getAccountID().longValue())//
|
||||||
|
.loanPortfolioAccountId(loansReceivableAccount.getAccountID().longValue())//
|
||||||
|
.transfersInSuspenseAccountId(suspenseAccount.getAccountID().longValue())//
|
||||||
|
.interestOnLoanAccountId(interestIncomeAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromFeeAccountId(feeIncomeAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromPenaltyAccountId(feeIncomeAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromRecoveryAccountId(recoveriesAccount.getAccountID().longValue())//
|
||||||
|
.writeOffAccountId(writtenOffAccount.getAccountID().longValue())//
|
||||||
|
.overpaymentLiabilityAccountId(overpaymentAccount.getAccountID().longValue())//
|
||||||
|
.receivableInterestAccountId(interestFeeReceivableAccount.getAccountID().longValue())//
|
||||||
|
.receivableFeeAccountId(interestFeeReceivableAccount.getAccountID().longValue())//
|
||||||
|
.receivablePenaltyAccountId(interestFeeReceivableAccount.getAccountID().longValue())//
|
||||||
|
.dateFormat(DATETIME_PATTERN)//
|
||||||
|
.locale("en_GB")//
|
||||||
|
.disallowExpectedDisbursements(true)//
|
||||||
|
.allowApprovedDisbursedAmountsOverApplied(true)//
|
||||||
|
.overAppliedCalculationType("percentage")//
|
||||||
|
.overAppliedNumber(50)//
|
||||||
|
.goodwillCreditAccountId(goodwillExpenseAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromGoodwillCreditInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromGoodwillCreditFeesAccountId(feeChargeOffAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromGoodwillCreditPenaltyAccountId(feeChargeOffAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromChargeOffInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromChargeOffFeesAccountId(feeChargeOffAccount.getAccountID().longValue())//
|
||||||
|
.chargeOffExpenseAccountId(creditLossBadDebtAccount.getAccountID().longValue())//
|
||||||
|
.chargeOffFraudExpenseAccountId(creditLossBadDebtFraudAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromChargeOffPenaltyAccountId(feeChargeOffAccount.getAccountID().longValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RequestSpecification createRequestSpecification() {
|
||||||
|
RequestSpecification request = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
|
||||||
|
request.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
|
||||||
|
request.header("Fineract-Platform-TenantId", "default");
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResponseSpecification createResponseSpecification(int statusCode) {
|
||||||
|
return new ResponseSpecBuilder().expectStatusCode(statusCode).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void verifyUndoLastDisbursalShallFail(Long loanId, String expectedError) {
|
||||||
|
ResponseSpecification errorResponse = new ResponseSpecBuilder().expectStatusCode(403).build();
|
||||||
|
LoanTransactionHelper validationErrorHelper = new LoanTransactionHelper(this.responseSpec, errorResponse);
|
||||||
|
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||||
|
validationErrorHelper.undoLastDisbursalLoan(loanId, new PostLoansLoanIdRequest());
|
||||||
|
});
|
||||||
|
assertTrue(exception.getMessage().contains(expectedError));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void verifyNoTransactions(Long loanId) {
|
||||||
|
verifyTransactions(loanId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void verifyTransactions(Long loanId, Transaction... transactions) {
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(responseSpec, requestSpec, loanId.intValue());
|
||||||
|
if (transactions == null || transactions.length == 0) {
|
||||||
|
assertNull(loanDetails.getTransactions(), "No transaction is expected");
|
||||||
|
} else {
|
||||||
|
Assertions.assertEquals(transactions.length, loanDetails.getTransactions().size());
|
||||||
|
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
|
||||||
|
Arrays.stream(transactions).forEach(tr -> {
|
||||||
|
boolean found = loanDetails.getTransactions().stream()
|
||||||
|
.anyMatch(item -> Objects.equals(item.getAmount(), tr.amount) && Objects.equals(item.getType().getValue(), tr.type)
|
||||||
|
&& Objects.equals(item.getDate(), LocalDate.parse(tr.date, dateTimeFormatter)));
|
||||||
|
Assertions.assertTrue(found, "Required transaction not found: " + tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void disburseLoan(Long loanId, BigDecimal amount, String date) {
|
||||||
|
loanTransactionHelper.disburseLoan(loanId, new PostLoansLoanIdRequest().actualDisbursementDate(date).dateFormat(DATETIME_PATTERN)
|
||||||
|
.transactionAmount(amount).locale("en"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void verifyJournalEntries(Long loanId, JournalEntry... entries) {
|
||||||
|
GetJournalEntriesTransactionIdResponse journalEntriesForLoan = journalEntryHelper.getJournalEntriesForLoan(loanId);
|
||||||
|
Assertions.assertEquals(entries.length, journalEntriesForLoan.getPageItems().size());
|
||||||
|
Arrays.stream(entries).forEach(journalEntry -> {
|
||||||
|
boolean found = journalEntriesForLoan.getPageItems().stream()
|
||||||
|
.anyMatch(item -> Objects.equals(item.getAmount(), journalEntry.amount)
|
||||||
|
&& Objects.equals(item.getGlAccountId(), journalEntry.account.getAccountID().longValue())
|
||||||
|
&& Objects.requireNonNull(item.getEntryType()).getValue().equals(journalEntry.type));
|
||||||
|
Assertions.assertTrue(found, "Required journal entry not found: " + journalEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void verifyRepaymentSchedule(Long loanId, Installment... installments) {
|
||||||
|
GetLoansLoanIdResponse loanResponse = loanTransactionHelper.getLoan(responseSpec, requestSpec, loanId.intValue());
|
||||||
|
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
|
||||||
|
|
||||||
|
Assertions.assertNotNull(loanResponse.getRepaymentSchedule());
|
||||||
|
Assertions.assertNotNull(loanResponse.getRepaymentSchedule().getPeriods());
|
||||||
|
Assertions.assertEquals(installments.length, loanResponse.getRepaymentSchedule().getPeriods().size(),
|
||||||
|
"Expected installments are not matching with the installments configured on the loan");
|
||||||
|
|
||||||
|
for (int i = 1; i < installments.length; i++) {
|
||||||
|
if (installments[i].completed == null) { // this is for the disbursement
|
||||||
|
Assertions.assertEquals(installments[i].amount,
|
||||||
|
loanResponse.getRepaymentSchedule().getPeriods().get(i).getPrincipalLoanBalanceOutstanding());
|
||||||
|
} else {
|
||||||
|
Assertions.assertEquals(installments[i].amount, loanResponse.getRepaymentSchedule().getPeriods().get(i).getPrincipalDue());
|
||||||
|
}
|
||||||
|
Assertions.assertEquals(installments[i].completed, loanResponse.getRepaymentSchedule().getPeriods().get(i).getComplete());
|
||||||
|
Assertions.assertEquals(LocalDate.parse(installments[i].dueDate, dateTimeFormatter),
|
||||||
|
loanResponse.getRepaymentSchedule().getPeriods().get(i).getDueDate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void runAt(String date, Runnable runnable) {
|
||||||
|
try {
|
||||||
|
GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(responseSpec, requestSpec, 42, true);
|
||||||
|
GlobalConfigurationHelper.updateIsBusinessDateEnabled(responseSpec, requestSpec, TRUE);
|
||||||
|
businessDateHelper.updateBusinessDate(
|
||||||
|
new BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en"));
|
||||||
|
runnable.run();
|
||||||
|
} finally {
|
||||||
|
GlobalConfigurationHelper.updateIsBusinessDateEnabled(responseSpec, requestSpec, FALSE);
|
||||||
|
GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(responseSpec, requestSpec, 42, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Long applyAndApproveLoan(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount) {
|
||||||
|
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(clientId)
|
||||||
|
.productId(loanProductId).expectedDisbursementDate(loanDisbursementDate).dateFormat(DATETIME_PATTERN)
|
||||||
|
.transactionProcessingStrategyCode(DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY)
|
||||||
|
.locale("en").submittedOnDate(loanDisbursementDate).amortizationType(1).interestRatePerPeriod(0)
|
||||||
|
.interestCalculationPeriodType(1).interestType(0).repaymentFrequencyType(0).repaymentEvery(30).repaymentFrequencyType(0)
|
||||||
|
.numberOfRepayments(1).loanTermFrequency(30).loanTermFrequencyType(0).maxOutstandingLoanBalance(BigDecimal.valueOf(amount))
|
||||||
|
.principal(BigDecimal.valueOf(amount)).loanType("individual"));
|
||||||
|
|
||||||
|
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||||
|
new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(amount)).dateFormat(DATETIME_PATTERN)
|
||||||
|
.approvedOnDate("01 January 2023").locale("en"));
|
||||||
|
|
||||||
|
return approvedLoanResult.getLoanId();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addRepaymentForLoan(Long loanId, Double amount, String date) {
|
||||||
|
String firstRepaymentUUID = UUID.randomUUID().toString();
|
||||||
|
loanTransactionHelper.makeLoanRepayment(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN)
|
||||||
|
.transactionDate(date).locale("en").transactionAmount(amount).externalId(firstRepaymentUUID));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JournalEntry journalEntry(double amount, Account account, String type) {
|
||||||
|
return new JournalEntry(amount, account, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Transaction transaction(double amount, String type, String date) {
|
||||||
|
return new Transaction(amount, type, date);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Installment installment(double amount, Boolean completed, String dueDate) {
|
||||||
|
return new Installment(amount, completed, dueDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ToString
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class Transaction {
|
||||||
|
|
||||||
|
Double amount;
|
||||||
|
String type;
|
||||||
|
String date;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ToString
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class JournalEntry {
|
||||||
|
|
||||||
|
Double amount;
|
||||||
|
Account account;
|
||||||
|
String type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ToString
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class Installment {
|
||||||
|
|
||||||
|
Double amount;
|
||||||
|
Boolean completed;
|
||||||
|
String dueDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1701
File diff suppressed because it is too large
Load Diff
+9
@@ -118,4 +118,13 @@ public class JournalEntryHelper {
|
|||||||
return GSON.fromJson(response, GetJournalEntriesTransactionIdResponse.class);
|
return GSON.fromJson(response, GetJournalEntriesTransactionIdResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GetJournalEntriesTransactionIdResponse getJournalEntriesForLoan(final Long loanId) {
|
||||||
|
log.info("Getting GL Journal entries for loan id {}", loanId);
|
||||||
|
final String url = "/fineract-provider/api/v1/journalentries?loanId=" + loanId + "&tenantIdentifier=default"
|
||||||
|
+ "&orderBy=id&sortOrder=desc&locale=en&dateFormat=dd MMMM yyyy";
|
||||||
|
final String response = Utils.performServerGet(this.requestSpec, this.responseSpec, url, null);
|
||||||
|
log.info("response {}", response);
|
||||||
|
return GSON.fromJson(response, GetJournalEntriesTransactionIdResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -38,6 +38,10 @@ public class LoanProductHelper extends IntegrationTest {
|
|||||||
return ok(fineract().loanProducts.retrieveLoanProductDetails1(externalId));
|
return ok(fineract().loanProducts.retrieveLoanProductDetails1(externalId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GetLoanProductsProductIdResponse retrieveLoanProductById(Long loanProductId) {
|
||||||
|
return ok(fineract().loanProducts.retrieveLoanProductDetails(loanProductId));
|
||||||
|
}
|
||||||
|
|
||||||
public PutLoanProductsProductIdResponse updateLoanProductByExternalId(String externalId, PutLoanProductsProductIdRequest request) {
|
public PutLoanProductsProductIdResponse updateLoanProductByExternalId(String externalId, PutLoanProductsProductIdRequest request) {
|
||||||
return ok(fineract().loanProducts.updateLoanProduct1(externalId, request));
|
return ok(fineract().loanProducts.updateLoanProduct1(externalId, request));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user