FINERACT-1905-Charges-Accrual-entries-on-created-date

This commit is contained in:
Ruchi Dhamankar
2023-05-16 11:53:32 +02:00
committed by Arnold Gálovics
parent 216b39ce0c
commit aaa94007ee
8 changed files with 859 additions and 6 deletions
@@ -136,4 +136,7 @@ public interface ConfigurationDomainService {
Long retrieveExternalEventBatchSize();
String retrieveReportExportS3FolderName();
String getAccrualDateConfigForCharge();
}
@@ -51,6 +51,7 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
private static final String REPORT_EXPORT_S3_FOLDER_NAME = "report-export-s3-folder-name";
public static final String CHARGE_ACCRUAL_DATE_CRITERIA = "charge-accrual-date";
private final PermissionRepository permissionRepository;
private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository;
private final PlatformCacheRepository cacheTypeRepository;
@@ -501,4 +502,16 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(REPORT_EXPORT_S3_FOLDER_NAME);
return property.getStringValue();
}
@Override
public String getAccrualDateConfigForCharge() {
String defaultValue = "due-date";
final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(CHARGE_ACCRUAL_DATE_CRITERIA);
String value = property.getStringValue();
if (StringUtils.isBlank(value)) {
return defaultValue;
}
return value;
}
}
@@ -31,6 +31,7 @@ import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent;
@@ -61,6 +62,8 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlatformService {
private static final String ACCRUAL_ON_CHARGE_DUE_DATE = "due-date";
private static final String ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE = "submitted-date";
private final LoanReadPlatformService loanReadPlatformService;
private final LoanChargeReadPlatformService loanChargeReadPlatformService;
private final JdbcTemplate jdbcTemplate;
@@ -71,6 +74,7 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanTransactionRepository loanTransactionRepository;
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
private final ConfigurationDomainService configurationDomainService;
@Override
@Transactional
@@ -244,8 +248,14 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
}
}
if (amount.compareTo(BigDecimal.ZERO) > 0) {
addAccrualAccounting(scheduleAccrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee, penaltyPortion,
totalAccPenalty, scheduleAccrualData.getDueDateAsLocaldate());
final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge();
if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_DUE_DATE)) {
addAccrualAccounting(scheduleAccrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee,
penaltyPortion, totalAccPenalty, scheduleAccrualData.getDueDateAsLocaldate());
} else if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) {
addAccrualAccounting(scheduleAccrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee,
penaltyPortion, totalAccPenalty, DateUtils.getBusinessLocalDate());
}
}
}
@@ -356,7 +366,62 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
private void updateCharges(final Collection<LoanChargeData> chargesData, final LoanScheduleAccrualData accrualData,
final LocalDate startDate, final LocalDate endDate) {
final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge();
if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_DUE_DATE)) {
updateChargeForDueDate(chargesData, accrualData, startDate, endDate);
} else if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) {
updateChargeForSubmittedOnDate(chargesData, accrualData, startDate, endDate);
}
}
private void updateChargeForSubmittedOnDate(Collection<LoanChargeData> chargesData, LoanScheduleAccrualData accrualData,
LocalDate startDate, LocalDate endDate) {
final Map<LoanChargeData, BigDecimal> applicableCharges = new HashMap<>();
BigDecimal submittedDateFeeIncome = BigDecimal.ZERO;
BigDecimal submittedDatePenaltyIncome = BigDecimal.ZERO;
LocalDate scheduleEndDate = accrualData.getDueDateAsLocaldate();
for (LoanChargeData loanCharge : chargesData) {
BigDecimal chargeAmount = BigDecimal.ZERO;
if (((accrualData.getInstallmentNumber() == 1 && loanCharge.getSubmittedOnDate().isEqual(startDate))
|| loanCharge.getSubmittedOnDate().isBefore(startDate) || loanCharge.getSubmittedOnDate().isEqual(startDate)
|| loanCharge.getSubmittedOnDate().isAfter(startDate)) && !loanCharge.getSubmittedOnDate().isAfter(endDate)
&& !loanCharge.getDueDate().isBefore(startDate) && !loanCharge.getDueDate().isAfter(scheduleEndDate)) {
chargeAmount = loanCharge.getAmount();
if (loanCharge.getAmountUnrecognized() != null) {
chargeAmount = chargeAmount.subtract(loanCharge.getAmountUnrecognized());
}
boolean canAddCharge = chargeAmount.compareTo(BigDecimal.ZERO) > 0;
if (canAddCharge && (loanCharge.getAmountAccrued() == null || chargeAmount.compareTo(loanCharge.getAmountAccrued()) != 0)) {
BigDecimal amountForAccrual = chargeAmount;
if (loanCharge.getAmountAccrued() != null) {
amountForAccrual = chargeAmount.subtract(loanCharge.getAmountAccrued());
}
applicableCharges.put(loanCharge, amountForAccrual);
}
}
if (loanCharge.isPenalty()) {
submittedDatePenaltyIncome = submittedDatePenaltyIncome.add(chargeAmount);
} else {
submittedDateFeeIncome = submittedDateFeeIncome.add(chargeAmount);
}
}
if (submittedDateFeeIncome.compareTo(BigDecimal.ZERO) == 0) {
submittedDateFeeIncome = null;
}
if (submittedDatePenaltyIncome.compareTo(BigDecimal.ZERO) == 0) {
submittedDatePenaltyIncome = null;
}
accrualData.updateChargeDetails(applicableCharges, submittedDateFeeIncome, submittedDatePenaltyIncome);
}
private void updateChargeForDueDate(Collection<LoanChargeData> chargesData, LoanScheduleAccrualData accrualData, LocalDate startDate,
LocalDate endDate) {
final Map<LoanChargeData, BigDecimal> applicableCharges = new HashMap<>();
BigDecimal dueDateFeeIncome = BigDecimal.ZERO;
BigDecimal dueDatePenaltyIncome = BigDecimal.ZERO;
@@ -146,6 +146,7 @@ import org.springframework.util.CollectionUtils;
@Transactional(readOnly = true)
public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
private static final String ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE = "submitted-date";
private final JdbcTemplate jdbcTemplate;
private final PlatformSecurityContext context;
private final LoanRepositoryWrapper loanRepositoryWrapper;
@@ -1739,9 +1740,17 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
@Override
public Collection<LoanScheduleAccrualData> retriveScheduleAccrualData() {
final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge();
if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) {
return retrieveScheduleAccrualDataForChargeSubmittedDateProcessing();
}
return retrieveScheduleAccrualDataForDefaultProcessing();
}
LoanScheduleAccrualMapper mapper = new LoanScheduleAccrualMapper();
private Collection<LoanScheduleAccrualData> retrieveScheduleAccrualDataForDefaultProcessing() {
LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate();
LoanScheduleAccrualMapper mapper = new LoanScheduleAccrualMapper();
Map<String, Object> paramMap = new HashMap<>(3);
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("select ").append(mapper.schema()).append(
" where (recaldet.is_compounding_to_be_posted_as_transaction is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ")
@@ -1749,16 +1758,40 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
.append(" or ( ls.penalty_charges_amount <> COALESCE(ls.accrual_penalty_charges_derived, 0))")
.append(" or ( ls.interest_amount <> COALESCE(ls.accrual_interest_derived, 0)))")
.append(" and loan.loan_status_id=:active and mpl.accounting_type=:type and loan.is_npa=false and ls.duedate <= :currentDate) ");
if (organisationStartDate != null) {
sqlBuilder.append(" and ls.duedate > :organisationStartDate ");
}
sqlBuilder.append(" order by loan.id,ls.duedate ");
Map<String, Object> paramMap = new HashMap<>(3);
paramMap.put("active", LoanStatus.ACTIVE.getValue());
paramMap.put("type", AccountingRuleType.ACCRUAL_PERIODIC.getValue());
paramMap.put("organisationStartDate", (organisationStartDate == null) ? DateUtils.getBusinessLocalDate() : organisationStartDate);
paramMap.put("currentDate", DateUtils.getBusinessLocalDate());
return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper);
}
private Collection<LoanScheduleAccrualData> retrieveScheduleAccrualDataForChargeSubmittedDateProcessing() {
LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate();
LoanScheduleAccrualMapper mapper = new LoanScheduleAccrualMapper();
Map<String, Object> paramMap = new HashMap<>(3);
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("select ").append(mapper.schema()).append(
" where (recaldet.is_compounding_to_be_posted_as_transaction is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ")
.append(" and (((ls.fee_charges_amount <> COALESCE(ls.accrual_fee_charges_derived, 0))")
.append(" or ( ls.penalty_charges_amount <> COALESCE(ls.accrual_penalty_charges_derived, 0))")
.append(" or ( ls.interest_amount <> COALESCE(ls.accrual_interest_derived, 0)))")
.append(" and loan.loan_status_id=:active and mpl.accounting_type=:type and loan.is_npa=false) ");
if (organisationStartDate != null) {
sqlBuilder.append(" and ls.duedate > :organisationStartDate ");
}
sqlBuilder.append(" order by loan.id,ls.duedate ");
paramMap.put("active", LoanStatus.ACTIVE.getValue());
paramMap.put("type", AccountingRuleType.ACCRUAL_PERIODIC.getValue());
paramMap.put("organisationStartDate", (organisationStartDate == null) ? DateUtils.getBusinessLocalDate() : organisationStartDate);
return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper);
}
@@ -1769,6 +1802,14 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
@Override
public Collection<LoanScheduleAccrualData> retrievePeriodicAccrualData(final LocalDate tillDate, final Loan loan) {
final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge();
if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) {
return retrievePeriodicAccrualDataForChargeSubmittedDateProcessing(tillDate, loan);
}
return retrievePeriodicAccrualDataForDefaultProcessing(tillDate, loan);
}
private Collection<LoanScheduleAccrualData> retrievePeriodicAccrualDataForDefaultProcessing(final LocalDate tillDate, final Loan loan) {
LoanSchedulePeriodicAccrualMapper mapper = new LoanSchedulePeriodicAccrualMapper();
LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate();
final StringBuilder sqlBuilder = new StringBuilder(400);
@@ -1793,7 +1834,34 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
paramMap.put("active", LoanStatus.ACTIVE.getValue());
paramMap.put("type", AccountingRuleType.ACCRUAL_PERIODIC.getValue());
paramMap.put("tillDate", tillDate);
return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper);
}
private Collection<LoanScheduleAccrualData> retrievePeriodicAccrualDataForChargeSubmittedDateProcessing(final LocalDate tillDate,
final Loan loan) {
LoanSchedulePeriodicAccrualMapper mapper = new LoanSchedulePeriodicAccrualMapper();
LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate();
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("select ").append(mapper.schema()).append(
" where (recaldet.is_compounding_to_be_posted_as_transaction is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ")
.append(" and (((ls.fee_charges_amount <> COALESCE(ls.accrual_fee_charges_derived, 0))")
.append(" or (ls.penalty_charges_amount <> COALESCE(ls.accrual_penalty_charges_derived, 0))")
.append(" or (ls.interest_amount <> COALESCE(ls.accrual_interest_derived, 0)))")
.append(" and loan.loan_status_id=:active and mpl.accounting_type=:type and (loan.closedon_date <= :tillDate or loan.closedon_date is null)")
.append(" and loan.is_npa=false)");
Map<String, Object> paramMap = new HashMap<>(5);
if (organisationStartDate != null) {
sqlBuilder.append(" and ls.duedate > :organisationStartDate ");
paramMap.put("organisationStartDate", organisationStartDate);
}
if (loan != null) {
sqlBuilder.append(" and loan.id= :loanId ");
paramMap.put("loanId", loan.getId());
}
sqlBuilder.append(" order by loan.id,ls.duedate ");
paramMap.put("active", LoanStatus.ACTIVE.getValue());
paramMap.put("type", AccountingRuleType.ACCRUAL_PERIODIC.getValue());
paramMap.put("tillDate", tillDate);
return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper);
}
@@ -126,4 +126,5 @@
<include file="parts/0104_loan_product_add_repayment_overdue_days_config.xml" relativeToChangelogFile="true" />
<include file="parts/0105_add_indices_loan_table.xml" relativeToChangelogFile="true" />
<include file="parts/0106_new_repayment_strategy.xml" relativeToChangelogFile="true" />
<include file="parts/0107_add_configuration_charges_accrual_date.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,41 @@
<?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.3.xsd">
<changeSet author="fineract" id="1" context="postgresql">
<sql>
SELECT SETVAL('c_configuration_id_seq', COALESCE(MAX(id), 0)+1, false ) FROM c_configuration;
</sql>
</changeSet>
<changeSet author="fineract" id="2">
<insert tableName="c_configuration">
<column name="name" value="charge-accrual-date"/>
<column name="value"/>
<column name="date_value"/>
<column name="string_value" value="due-date"/>
<column name="enabled" valueBoolean="true"/>
<column name="is_trap_door" valueBoolean="false"/>
<column name="description" value="due-date: default for due-date, Use comma seperated values for due-date, submitted-date"/>
</insert>
</changeSet>
</databaseChangeLog>