FINERACT-2104: Accrual Activity Posting for Loans

This commit is contained in:
Soma Sörös
2024-07-09 11:32:31 +02:00
committed by Adam Saghy
parent 67440c47ab
commit 3f4bbf3e14
27 changed files with 620 additions and 30 deletions
@@ -0,0 +1,82 @@
/**
* 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.cob.loan;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class AccrualActivityPostingBusinessStep implements LoanCOBBusinessStep {
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanAccountDomainService loanAccountDomainService;
private final ExternalIdFactory externalIdFactory;
private final LoanWritePlatformService loanWritePlatformService;
@Override
public Loan execute(Loan loan) {
log.debug("start processing loan accrual activity posting on installment due date with id [{}]", loan.getId());
// check if loan capable for posting
if (loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) {
final LocalDate currentDate = DateUtils.getBusinessLocalDate();
// check if loan has installment due on business day
Optional<LoanRepaymentScheduleInstallment> first = loan.getRepaymentScheduleInstallments().stream()
.filter(loanRepaymentScheduleInstallment -> loanRepaymentScheduleInstallment.getDueDate().isEqual(currentDate))
.findFirst();
if (first.isPresent()) {
final LoanRepaymentScheduleInstallment installment = first.get();
// check if there is no not-replayed-accrual-activity related to business date
List<LoanTransaction> loanTransactions = loan.getLoanTransactions(loanTransaction -> loanTransaction.isNotReversed()
&& loanTransaction.isAccrualActivity() && loanTransaction.getTransactionDate().isEqual(currentDate));
if (loanTransactions.isEmpty()) {
loan = loanWritePlatformService.makeAccrualActivityTransaction(loan, installment, currentDate);
}
}
}
log.debug("end processing loan accrual activity posting on installment due date with id [{}]", loan.getId());
return loan;
}
@Override
public String getEnumStyledName() {
return "ACCRUAL_ACTIVITY_POSTING";
}
@Override
public String getHumanReadableName() {
return "Accrual Activity Posting on Installment Due Date";
}
}
@@ -511,7 +511,8 @@ public class LoanScheduleAssembler {
allowCompoundingOnEod, isEqualAmortization, isInterestToBeRecoveredFirstWhenGreaterThanEMI,
fixedPrincipalPercentagePerInstallment, isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled,
disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate,
loanScheduleType, loanScheduleProcessingType, fixedLength);
loanScheduleType, loanScheduleProcessingType, fixedLength,
loanProduct.getLoanProductRelatedDetail().isEnableAccrualActivityPosting());
}
private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery,
@@ -26,9 +26,11 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
@@ -65,4 +67,17 @@ public class LoanTransactionAssembler {
return LoanTransaction.repaymentType(repaymentTransactionType, loan.getOffice(), repaymentAmount, paymentDetail, transactionDate,
txnExternalId, null);
}
public LoanTransaction assembleAccrualActivityTransaction(Loan loan, final LoanRepaymentScheduleInstallment installment,
final LocalDate transactionDate) {
MonetaryCurrency currency = loan.getCurrency();
ExternalId externalId = externalIdFactory.create();
BigDecimal interestPortion = installment.getInterestCharged(currency).getAmount();
BigDecimal feeChargesPortion = installment.getFeeChargesCharged(currency).getAmount();
BigDecimal penaltyChargesPortion = installment.getPenaltyChargesCharged(currency).getAmount();
BigDecimal transactionAmount = interestPortion.add(feeChargesPortion).add(penaltyChargesPortion);
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.ACCRUAL_ACTIVITY.getValue(), transactionDate,
transactionAmount, null, interestPortion, feeChargesPortion, penaltyChargesPortion, null, false, null, externalId);
}
}
@@ -84,6 +84,8 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanDisbursalTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionAccrualActivityPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionAccrualActivityPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPreBusinessEvent;
@@ -1131,6 +1133,25 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
return changedTransactionDetail;
}
@Transactional
@Override
public Loan makeAccrualActivityTransaction(Loan loan, final LoanRepaymentScheduleInstallment installment,
final LocalDate transactionDate) {
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionAccrualActivityPreBusinessEvent(loan));
LoanTransaction newAccrualActivityTransaction = loanTransactionAssembler.assembleAccrualActivityTransaction(loan, installment,
transactionDate);
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(newAccrualActivityTransaction);
loan.addLoanTransaction(newAccrualActivityTransaction);
businessEventNotifierService
.notifyPostBusinessEvent(new LoanTransactionAccrualActivityPostBusinessEvent(newAccrualActivityTransaction));
return loan;
}
@Transactional
@Override
public CommandProcessingResult makeInterestPaymentWaiver(final JsonCommand command) {
@@ -106,6 +106,8 @@ public final class LoanProductDataValidator {
public static final String MAX_DIFFERENTIAL_LENDING_RATE = "maxDifferentialLendingRate";
public static final String IS_FLOATING_INTEREST_RATE_CALCULATION_ALLOWED = "isFloatingInterestRateCalculationAllowed";
public static final String ACCOUNTING_RULE = "accountingRule";
public static final String ENABLE_ACCRUAL_ACTIVITY_POSTING = "enableAccrualActivityPosting";
/**
* The parameters supported for this command.
*/
@@ -172,7 +174,8 @@ public final class LoanProductDataValidator {
LoanProductConstants.ENABLE_DOWN_PAYMENT, LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT,
LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, LoanProductConstants.REPAYMENT_START_DATE_TYPE,
LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, LoanProductConstants.LOAN_SCHEDULE_TYPE,
LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, LoanProductConstants.FIXED_LENGTH));
LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, LoanProductConstants.FIXED_LENGTH,
LoanProductConstants.ENABLE_ACCRUAL_ACTIVITY_POSTING));
private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName,
LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName,
@@ -734,6 +737,18 @@ public final class LoanProductDataValidator {
.value(receivablePenaltyAccountId).notNull().integerGreaterThanZero();
}
if (!AccountingValidations.isAccrualBasedAccounting(accountingRuleType)
&& this.fromApiJsonHelper.parameterExists(LoanProductConstants.ENABLE_ACCRUAL_ACTIVITY_POSTING, element)) {
final Boolean enableAccrualActivityPosting = this.fromApiJsonHelper
.extractBooleanNamed(LoanProductConstants.ENABLE_ACCRUAL_ACTIVITY_POSTING, element);
baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_ACCRUAL_ACTIVITY_POSTING).value(enableAccrualActivityPosting)
.ignoreIfNull().validateForBooleanValue();
if (!AccountingValidations.isAccrualBasedAccounting(accountingRuleType)) {
baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_ACCRUAL_ACTIVITY_POSTING).isOneOfTheseValues(Boolean.TRUE)
.failWithCode("should be false for non accrual accounting");
}
}
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME, element)) {
final Boolean useBorrowerCycle = this.fromApiJsonHelper
.extractBooleanNamed(LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME, element);
@@ -1613,6 +1628,25 @@ public final class LoanProductDataValidator {
final Integer accountingRuleType = this.fromApiJsonHelper.extractIntegerNamed(ACCOUNTING_RULE, element, Locale.getDefault());
baseDataValidator.reset().parameter(ACCOUNTING_RULE).value(accountingRuleType).ignoreIfNull().inMinMaxRange(1, 4);
boolean actualValue = loanProduct.getLoanProductRelatedDetail().isEnableAccrualActivityPosting();
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.ENABLE_ACCRUAL_ACTIVITY_POSTING, element)) {
final Boolean enableAccrualActivityPosting = this.fromApiJsonHelper
.extractBooleanNamed(LoanProductConstants.ENABLE_ACCRUAL_ACTIVITY_POSTING, element);
baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_ACCRUAL_ACTIVITY_POSTING).value(enableAccrualActivityPosting)
.ignoreIfNull().validateForBooleanValue();
actualValue = enableAccrualActivityPosting;
}
if (actualValue) {
Integer ruleType = accountingRuleType;
if (ruleType == null) {
ruleType = loanProduct.getAccountingRule();
}
if (!AccountingValidations.isAccrualBasedAccounting(ruleType)) {
baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_ACCRUAL_ACTIVITY_POSTING)
.failWithCode("should be false for non accrual accounting");
}
}
final Long fundAccountId = this.fromApiJsonHelper.extractLongNamed(LoanProductAccountingParams.FUND_SOURCE.getValue(), element);
baseDataValidator.reset().parameter(LoanProductAccountingParams.FUND_SOURCE.getValue()).value(fundAccountId).ignoreIfNull()
.integerGreaterThanZero();
@@ -234,7 +234,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
+ "lp.nominal_interest_rate_per_period as interestRatePerPeriod, lp.min_nominal_interest_rate_per_period as minInterestRatePerPeriod, lp.max_nominal_interest_rate_per_period as maxInterestRatePerPeriod, lp.interest_period_frequency_enum as interestRatePerPeriodFreq, "
+ "lp.annual_nominal_interest_rate as annualInterestRate, lp.interest_method_enum as interestMethod, lp.interest_calculated_in_period_enum as interestCalculationInPeriodMethod,lp.allow_partial_period_interest_calcualtion as allowPartialPeriodInterestCalcualtion, "
+ "lp.repay_every as repaidEvery, lp.repayment_period_frequency_enum as repaymentPeriodFrequency, lp.number_of_repayments as numberOfRepayments, lp.min_number_of_repayments as minNumberOfRepayments, lp.max_number_of_repayments as maxNumberOfRepayments, "
+ "lp.fixed_length as fixedLength, "
+ "lp.fixed_length as fixedLength, " + "lp.enable_accrual_activity_posting as enableAccrualActivityPosting, "
+ "lp.grace_on_principal_periods as graceOnPrincipalPayment, lp.recurring_moratorium_principal_periods as recurringMoratoriumOnPrincipalPeriods, lp.grace_on_interest_periods as graceOnInterestPayment, lp.grace_interest_free_periods as graceOnInterestCharged,lp.grace_on_arrears_ageing as graceOnArrearsAgeing,lp.overdue_days_for_npa as overdueDaysForNPA, "
+ "lp.min_days_between_disbursal_and_first_repayment As minimumDaysBetweenDisbursalAndFirstRepayment, "
+ "lp.amortization_method_enum as amortizationMethod, lp.arrearstolerance_amount as tolerance, "
@@ -525,6 +525,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
final LoanScheduleType loanScheduleType = LoanScheduleType.valueOf(loanScheduleTypeStr);
final String loanScheduleProcessingTypeStr = rs.getString("loanScheduleProcessingType");
final LoanScheduleProcessingType loanScheduleProcessingType = LoanScheduleProcessingType.valueOf(loanScheduleProcessingTypeStr);
final boolean enableAccrualActivityPosting = rs.getBoolean("enableAccrualActivityPosting");
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -547,7 +548,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageForDownPayment,
enableAutoRepaymentForDownPayment, advancedPaymentData, creditAllocationData, repaymentStartDateType,
enableInstallmentLevelDelinquency, loanScheduleType.asEnumOptionData(), loanScheduleProcessingType.asEnumOptionData(),
fixedLength);
fixedLength, enableAccrualActivityPosting);
}
}
@@ -161,4 +161,5 @@
<include file="parts/0139_add_disburse_without_auto_payment_command.xml" relativeToChangelogFile="true" />
<include file="parts/0140_trial_balance_with_asset_transfer_update.xml" relativeToChangelogFile="true" />
<include file="parts/0141_add_interest_payment_waiver_transaction_type.xml" relativeToChangelogFile="true" />
<include file="parts/0142_add_accrual_activity_transaction.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_product_loan">
<column defaultValueBoolean="false" type="boolean" name="enable_accrual_activity_posting">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet author="fineract" id="2">
<addColumn tableName="m_loan">
<column defaultValueBoolean="false" type="boolean" name="enable_accrual_activity_posting">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="3" author="fineract">
<insert tableName="m_external_event_configuration">
<column name="type" value="LoanTransactionAccrualActivityPostBusinessEvent"/>
<column name="enabled" valueBoolean="false"/>
</insert>
<insert tableName="m_external_event_configuration">
<column name="type" value="LoanTransactionAccrualActivityPreBusinessEvent"/>
<column name="enabled" valueBoolean="false"/>
</insert>
<insert tableName="r_enum_value">
<column name="enum_name" value="transaction_type_enum"/>
<column name="enum_id" valueNumeric="32"/>
<column name="enum_message_property" value="Accrual Activity"/>
<column name="enum_value" value="Accrual Activity"/>
<column name="enum_type" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet id="4" author="fineract">
<insert tableName="m_batch_business_steps">
<column name="job_name" value="LOAN_CLOSE_OF_BUSINESS"/>
<column name="step_name" value="ACCRUAL_ACTIVITY_POSTING"/>
<column name="step_order" value="8"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -102,7 +102,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanReAgeTransactionBusinessEvent", "LoanUndoReAgeTransactionBusinessEvent", "LoanReAmortizeTransactionBusinessEvent",
"LoanUndoReAmortizeTransactionBusinessEvent", "LoanReAgeBusinessEvent", "LoanUndoReAgeBusinessEvent",
"LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent", "LoanTransactionInterestPaymentWaiverPreBusinessEvent",
"LoanTransactionInterestPaymentWaiverPostBusinessEvent");
"LoanTransactionInterestPaymentWaiverPostBusinessEvent", "LoanTransactionAccrualActivityPostBusinessEvent",
"LoanTransactionAccrualActivityPreBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -186,7 +187,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanReAgeTransactionBusinessEvent", "LoanUndoReAgeTransactionBusinessEvent", "LoanReAmortizeTransactionBusinessEvent",
"LoanUndoReAmortizeTransactionBusinessEvent", "LoanReAgeBusinessEvent", "LoanUndoReAgeBusinessEvent",
"LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent", "LoanTransactionInterestPaymentWaiverPreBusinessEvent",
"LoanTransactionInterestPaymentWaiverPostBusinessEvent");
"LoanTransactionInterestPaymentWaiverPostBusinessEvent", "LoanTransactionAccrualActivityPostBusinessEvent",
"LoanTransactionAccrualActivityPreBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -95,7 +95,7 @@ public class DefaultScheduledDateGeneratorTest {
Money.of(fromApplicationCurrency(dollarCurrency), ZERO), false, null, EMPTY_LIST, BigDecimal.valueOf(36_000L), null,
DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null, null, null, null, null, ZERO, null, NONE, null, ZERO,
EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false, false, null, false, false, null, false, DISBURSEMENT_DATE,
submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null);
submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false);
// when
List<PreGeneratedLoanSchedulePeriod> result = underTest.generateRepaymentPeriods(expectedDisbursementDate, loanApplicationTerms,
@@ -166,7 +166,7 @@ public class DefaultScheduledDateGeneratorTest {
null, null, null, null, null, Money.of(fromApplicationCurrency(dollarCurrency), ZERO), false, null, EMPTY_LIST,
BigDecimal.valueOf(36_000L), null, DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null, null, null, null, null, ZERO,
null, NONE, null, ZERO, EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false, false, null, false, false, null, false,
DISBURSEMENT_DATE, submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null);
DISBURSEMENT_DATE, submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false);
}
private HolidayDetailDTO createHolidayDTO() {