FINERACT-2134: Missing charge paid by references and journal entries for disbursement occuring accruals
This commit is contained in:
committed by
Arnold Gálovics
parent
39d95f393b
commit
33f666ccc3
+13
@@ -1267,6 +1267,19 @@ final class LoansApiResourceSwagger {
|
||||
public BigDecimal disbursedAmountPercentageForDownPayment;
|
||||
@Schema(example = "false")
|
||||
public Boolean enableAutoRepaymentForDownPayment;
|
||||
|
||||
public List<PostLoansRequestChargeData> charges;
|
||||
|
||||
static final class PostLoansRequestChargeData {
|
||||
|
||||
private PostLoansRequestChargeData() {}
|
||||
|
||||
@Schema(example = "1")
|
||||
public Long chargeId;
|
||||
|
||||
@Schema(example = "1.0")
|
||||
public BigDecimal amount;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(description = "PostLoansResponse")
|
||||
|
||||
+12
-2
@@ -152,6 +152,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonit
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
|
||||
@@ -330,8 +331,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
|
||||
businessEventNotifierService.notifyPreBusinessEvent(new LoanDisbursalBusinessEvent(loan));
|
||||
|
||||
final List<Long> existingTransactionIds = new ArrayList<>();
|
||||
final List<Long> existingReversedTransactionIds = new ArrayList<>();
|
||||
List<Long> existingTransactionIds = new ArrayList<>();
|
||||
List<Long> existingReversedTransactionIds = new ArrayList<>();
|
||||
|
||||
final AppUser currentUser = getAppUserIfPresent();
|
||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||
@@ -489,6 +490,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||
}
|
||||
|
||||
existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
|
||||
final Set<LoanCharge> loanCharges = loan.getActiveCharges();
|
||||
final Map<Long, BigDecimal> disBuLoanCharges = new HashMap<>();
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
@@ -499,10 +502,17 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
if (loanCharge.isDisbursementCharge()) {
|
||||
LoanTransaction loanTransaction = LoanTransaction.accrueTransaction(loan, loan.getOffice(), actualDisbursementDate,
|
||||
loanCharge.amount(), null, loanCharge.amount(), null, externalIdFactory.create());
|
||||
loanTransaction.updateLoan(loan);
|
||||
LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(loanTransaction, loanCharge,
|
||||
loanCharge.getAmount(loan.getCurrency()).getAmount(), 1);
|
||||
loanTransaction.getLoanChargesPaid().add(loanChargePaidBy);
|
||||
loan.addLoanTransaction(loanTransaction);
|
||||
LoanTransaction savedLoanTransaction = loanTransactionRepository.saveAndFlush(loanTransaction);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(savedLoanTransaction));
|
||||
}
|
||||
}
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
|
||||
for (final Map.Entry<Long, BigDecimal> entrySet : disBuLoanCharges.entrySet()) {
|
||||
final PortfolioAccountData savingAccountData = this.accountAssociationsReadPlatformService.retriveLoanLinkedAssociation(loanId);
|
||||
final SavingsAccount fromSavingsAccount = null;
|
||||
|
||||
+41
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.integrationtests;
|
||||
|
||||
import static java.lang.System.lineSeparator;
|
||||
import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
|
||||
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.TransactionProcessingStrategyCode.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
|
||||
import static org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY;
|
||||
@@ -26,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
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 static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import io.restassured.builder.RequestSpecBuilder;
|
||||
import io.restassured.builder.ResponseSpecBuilder;
|
||||
@@ -39,6 +41,7 @@ import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
@@ -59,6 +62,7 @@ import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
|
||||
import org.apache.fineract.client.models.JournalEntryTransactionItem;
|
||||
import org.apache.fineract.client.models.PaymentAllocationOrder;
|
||||
import org.apache.fineract.client.models.PostChargesResponse;
|
||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||
@@ -641,6 +645,9 @@ public abstract class BaseLoanIntegrationTest {
|
||||
loanTransactionHelper.undoLastDisbursalLoan(loanId, new PostLoansLoanIdRequest());
|
||||
}
|
||||
|
||||
// Note: this is buggy because if multiple journal entries are for the same account, amount and type, the
|
||||
// verification will pass
|
||||
// not all journal entries have been validated - since there might be duplicates
|
||||
protected void verifyJournalEntries(Long loanId, Journal... entries) {
|
||||
GetJournalEntriesTransactionIdResponse journalEntriesForLoan = journalEntryHelper.getJournalEntriesForLoan(loanId);
|
||||
Assertions.assertEquals(entries.length, journalEntriesForLoan.getPageItems().size());
|
||||
@@ -653,6 +660,27 @@ public abstract class BaseLoanIntegrationTest {
|
||||
});
|
||||
}
|
||||
|
||||
protected void verifyJournalEntriesSequentially(Long loanId, Journal... entries) {
|
||||
GetJournalEntriesTransactionIdResponse journalEntriesForLoan = journalEntryHelper.getJournalEntriesForLoan(loanId);
|
||||
List<JournalEntryTransactionItem> sortedJournalEntries = journalEntriesForLoan.getPageItems().stream()
|
||||
.sorted(Comparator.comparing(JournalEntryTransactionItem::getId)).toList();
|
||||
for (int i = 0; i < entries.length && i < journalEntriesForLoan.getPageItems().size(); i++) {
|
||||
Journal journalEntry = entries[i];
|
||||
JournalEntryTransactionItem item = sortedJournalEntries.get(i);
|
||||
boolean found = Objects.equals(item.getAmount(), journalEntry.amount)
|
||||
&& Objects.equals(item.getGlAccountId(), journalEntry.account.getAccountID().longValue())
|
||||
&& Objects.requireNonNull(item.getEntryType()).getValue().equals(journalEntry.type);
|
||||
assertTrue(found, "Journal entry mismatch at position " + i + "." + lineSeparator() + "Wanted Journal entry: " + journalEntry
|
||||
+ lineSeparator() + "Actual Journal entry: " + item);
|
||||
}
|
||||
if (journalEntriesForLoan.getPageItems().size() > entries.length) {
|
||||
fail("Some Journal Entries are not verified. The missing entries are here: "
|
||||
+ sortedJournalEntries.subList(entries.length, sortedJournalEntries.size()));
|
||||
}
|
||||
Assertions.assertEquals(entries.length, journalEntriesForLoan.getPageItems().size(),
|
||||
"There were more journal entries expected than actually present.");
|
||||
}
|
||||
|
||||
protected void verifyTRJournalEntries(Long transactionId, Journal... entries) {
|
||||
GetJournalEntriesTransactionIdResponse journalEntriesForLoan = journalEntryHelper.getJournalEntries("L" + transactionId.toString());
|
||||
Assertions.assertEquals(entries.length, journalEntriesForLoan.getPageItems().size());
|
||||
@@ -675,6 +703,13 @@ public abstract class BaseLoanIntegrationTest {
|
||||
return loanChargeId.longValue();
|
||||
}
|
||||
|
||||
protected Long createDisbursementPercentageCharge(double percentageAmount) {
|
||||
Integer chargeId = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper
|
||||
.getLoanDisbursementJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_PERCENTAGE_AMOUNT, String.valueOf(percentageAmount)));
|
||||
assertNotNull(chargeId);
|
||||
return chargeId.longValue();
|
||||
}
|
||||
|
||||
protected void verifyRepaymentSchedule(Long loanId, Installment... installments) {
|
||||
GetLoansLoanIdResponse loanResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
|
||||
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
|
||||
@@ -1192,6 +1227,11 @@ public abstract class BaseLoanIntegrationTest {
|
||||
public static final Integer FLAT = 1;
|
||||
}
|
||||
|
||||
public static class InterestRecalculationCompoundingMethod {
|
||||
|
||||
public static final Integer NONE = 0;
|
||||
}
|
||||
|
||||
public static class RepaymentFrequencyType {
|
||||
|
||||
public static final Integer MONTHS = 2;
|
||||
@@ -1227,6 +1267,7 @@ public abstract class BaseLoanIntegrationTest {
|
||||
|
||||
public static class RescheduleStrategyMethod {
|
||||
|
||||
public static final Integer REDUCE_EMI_AMOUNT = 3;
|
||||
public static final Integer ADJUST_LAST_UNPAID_PERIOD = 4;
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -18,6 +18,9 @@
|
||||
*/
|
||||
package org.apache.fineract.integrationtests.common.accounting;
|
||||
|
||||
import lombok.ToString;
|
||||
|
||||
@ToString
|
||||
public class Account {
|
||||
|
||||
public enum AccountType {
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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.loan.repayment;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.client.models.ChargeData;
|
||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.PostLoansRequest;
|
||||
import org.apache.fineract.client.models.PostLoansRequestChargeData;
|
||||
import org.apache.fineract.client.models.PostLoansResponse;
|
||||
import org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
|
||||
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class LoanRepaymentTest extends BaseLoanIntegrationTest {
|
||||
|
||||
@Test
|
||||
public void test_LoanRepaymentWorks_WhenDisbursementChargeIsAvailable_AndAccrualAccounting_AndDailyRecalculateInterest_AndDailyInterestCalculationPeriod() {
|
||||
|
||||
runAt("01 January 2023", () -> {
|
||||
// Create Client
|
||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
|
||||
int numberOfRepayments = 3;
|
||||
int repaymentEvery = 1;
|
||||
|
||||
// Create charges
|
||||
double charge1Amount = 1.0;
|
||||
double charge2Amount = 1.5;
|
||||
Long charge1Id = createDisbursementPercentageCharge(charge1Amount);
|
||||
Long charge2Id = createDisbursementPercentageCharge(charge2Amount);
|
||||
|
||||
// Create Loan Product
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
|
||||
.numberOfRepayments(numberOfRepayments) //
|
||||
.repaymentEvery(repaymentEvery) //
|
||||
.installmentAmountInMultiplesOf(null) //
|
||||
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) //
|
||||
.interestType(InterestType.DECLINING_BALANCE)//
|
||||
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
|
||||
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
|
||||
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
|
||||
.isInterestRecalculationEnabled(true)//
|
||||
.recalculationRestFrequencyInterval(1)//
|
||||
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
|
||||
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
|
||||
.allowPartialPeriodInterestCalcualtion(false)//
|
||||
.disallowExpectedDisbursements(false)//
|
||||
.allowApprovedDisbursedAmountsOverApplied(false)//
|
||||
.overAppliedNumber(null)//
|
||||
.overAppliedCalculationType(null)//
|
||||
.multiDisburseLoan(null)//
|
||||
.charges(List.of(new ChargeData().id(charge1Id), new ChargeData().id(charge2Id)));//
|
||||
|
||||
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||
Long loanProductId = loanProductResponse.getResourceId();
|
||||
|
||||
// Apply and Approve Loan
|
||||
double amount = 1000.0;
|
||||
|
||||
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
|
||||
.repaymentEvery(repaymentEvery)//
|
||||
.loanTermFrequency(numberOfRepayments)//
|
||||
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
|
||||
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS)//
|
||||
.interestType(InterestType.DECLINING_BALANCE)//
|
||||
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
|
||||
.charges(List.of(//
|
||||
new PostLoansRequestChargeData().chargeId(charge1Id).amount(BigDecimal.valueOf(charge1Amount)), //
|
||||
new PostLoansRequestChargeData().chargeId(charge2Id).amount(BigDecimal.valueOf(charge2Amount))//
|
||||
));//
|
||||
|
||||
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||
|
||||
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||
approveLoanRequest(amount, "01 January 2023"));
|
||||
|
||||
Long loanId = approvedLoanResult.getLoanId();
|
||||
|
||||
// disburse Loan
|
||||
disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 January 2023");
|
||||
|
||||
// verify transactions
|
||||
verifyTransactions(loanId, //
|
||||
transaction(1000.0, "Disbursement", "01 January 2023"), //
|
||||
transaction(25.0, "Repayment (at time of disbursement)", "01 January 2023"), //
|
||||
transaction(10.0, "Accrual", "01 January 2023"), //
|
||||
transaction(15.0, "Accrual", "01 January 2023") //
|
||||
);
|
||||
|
||||
// verify journal entries
|
||||
verifyJournalEntries(loanId, //
|
||||
journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
|
||||
journalEntry(1000.0, fundSource, "CREDIT"), //
|
||||
journalEntry(25.0, feeIncomeAccount, "CREDIT"), //
|
||||
journalEntry(25.0, fundSource, "DEBIT"), //
|
||||
journalEntry(10.0, feeReceivableAccount, "DEBIT"), //
|
||||
journalEntry(10.0, feeIncomeAccount, "CREDIT"), //
|
||||
journalEntry(15.0, feeReceivableAccount, "DEBIT"), //
|
||||
journalEntry(15.0, feeIncomeAccount, "CREDIT") //
|
||||
);
|
||||
|
||||
// repay 500
|
||||
addRepaymentForLoan(loanId, 500.0, "01 January 2023");
|
||||
|
||||
// verify transactions
|
||||
verifyTransactions(loanId, //
|
||||
transaction(1000.0, "Disbursement", "01 January 2023"), //
|
||||
transaction(25.0, "Repayment (at time of disbursement)", "01 January 2023"), //
|
||||
transaction(500.0, "Repayment", "01 January 2023") //
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user