Skip to content

Commit

Permalink
FINERACT-2134: Missing charge paid by references and journal entries …
Browse files Browse the repository at this point in the history
…for disbursement occuring accruals
  • Loading branch information
galovics committed Oct 2, 2024
1 parent 39d95f3 commit 33f666c
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,19 @@ private PostLoansRequest() {}
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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
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;
Expand Down Expand Up @@ -330,8 +331,8 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand

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<>();
Expand Down Expand Up @@ -489,6 +490,8 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand
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) {
Expand All @@ -499,10 +502,17 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,6 +27,7 @@
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;
Expand All @@ -39,6 +41,7 @@
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;
Expand All @@ -59,6 +62,7 @@
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;
Expand Down Expand Up @@ -641,6 +645,9 @@ protected void undoLastDisbursement(Long loanId) {
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());
Expand All @@ -653,6 +660,27 @@ protected void verifyJournalEntries(Long loanId, Journal... entries) {
});
}

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());
Expand All @@ -675,6 +703,13 @@ protected Long addCharge(Long loanId, boolean isPenalty, double amount, String d
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);
Expand Down Expand Up @@ -1192,6 +1227,11 @@ public static class InterestType {
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;
Expand Down Expand Up @@ -1227,6 +1267,7 @@ public static class TransactionProcessingStrategyCode {

public static class RescheduleStrategyMethod {

public static final Integer REDUCE_EMI_AMOUNT = 3;
public static final Integer ADJUST_LAST_UNPAID_PERIOD = 4;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
*/
package org.apache.fineract.integrationtests.common.accounting;

import lombok.ToString;

@ToString
public class Account {

public enum AccountType {
Expand Down
Original file line number Diff line number Diff line change
@@ -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") //
);
});
}
}

0 comments on commit 33f666c

Please sign in to comment.