FINERACT-2312: Accruals added for savings accounts (#4885)

This commit is contained in:
Christopher Sayhaed Giron Vega
2025-08-20 15:00:09 -06:00
committed by GitHub
parent c936c19b85
commit b349cf6831
24 changed files with 1074 additions and 5 deletions
@@ -0,0 +1,45 @@
/**
* 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.portfolio.savings.data;
import java.time.LocalDate;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.service.SavingsEnumerations;
@Data
@RequiredArgsConstructor
public class SavingsAccrualData {
private final Long id;
private final String accountNo;
private final LocalDate accruedTill;
private final Boolean isTypeInterestReceivable;
private final Boolean isAllowOverdraft;
private final Integer depositType;
public DepositAccountType getDepositType() {
final EnumOptionData depositType = SavingsEnumerations.depositType(this.depositType);
DepositAccountType depositAccountType = DepositAccountType.fromInt(depositType.getId().intValue());
return depositAccountType;
}
}
@@ -71,6 +71,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer;
@@ -337,6 +338,9 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
@JoinColumn(name = "tax_group_id")
private TaxGroup taxGroup;
@Column(name = "accrued_till_date")
private LocalDate accruedTillDate;
@Column(name = "total_savings_amount_on_hold", scale = 6, precision = 19, nullable = true)
private BigDecimal savingsOnHoldAmount;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "account", orphanRemoval = true, fetch = FetchType.LAZY)
@@ -926,6 +930,10 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
return this.nominalAnnualInterestRateOverdraft.divide(BigDecimal.valueOf(100L), mc);
}
public BigDecimal getEffectiveInterestRateAsFractionAccrual(final MathContext mc, final LocalDate upToInterestCalculationDate) {
return this.nominalAnnualInterestRate.divide(BigDecimal.valueOf(100L), mc);
}
@SuppressWarnings("unused")
protected BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate upToInterestCalculationDate) {
return this.nominalAnnualInterestRate.divide(BigDecimal.valueOf(100L), mc);
@@ -939,6 +947,11 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
return isAllowOverdraft() && !MathUtil.isEmpty(getOverdraftLimit()) && !MathUtil.isEmpty(nominalAnnualInterestRateOverdraft);
}
public List<SavingsAccountTransaction> retrieveOrderedAccrualTransactions() {
return retrieveListOfTransactions().stream().filter(SavingsAccountTransaction::isAccrual)
.sorted(new SavingsAccountTransactionComparator()).collect(Collectors.toList());
}
protected List<SavingsAccountTransaction> retreiveOrderedNonInterestPostingTransactions() {
final List<SavingsAccountTransaction> listOfTransactionsSorted = retrieveListOfTransactions();
@@ -3841,10 +3854,21 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
return this.withHoldTax;
}
public void setAccruedTillDate(LocalDate accruedTillDate) {
this.accruedTillDate = accruedTillDate;
}
public List<SavingsAccountTransactionDetailsForPostingPeriod> toSavingsAccountTransactionDetailsForPostingPeriodList(
List<SavingsAccountTransaction> transactions) {
return transactions.stream()
.map(transaction -> transaction.toSavingsAccountTransactionDetailsForPostingPeriod(this.currency, this.allowOverdraft))
.toList();
}
public List<SavingsAccountTransactionDetailsForPostingPeriod> toSavingsAccountTransactionDetailsForPostingPeriodList() {
return retreiveOrderedNonInterestPostingTransactions().stream()
.map(transaction -> transaction.toSavingsAccountTransactionDetailsForPostingPeriod(this.currency, this.allowOverdraft))
.toList();
}
}
@@ -19,8 +19,10 @@
package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.LockModeType;
import java.time.LocalDate;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.portfolio.savings.data.SavingsAccrualData;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -70,4 +72,26 @@ public interface SavingsAccountRepository extends JpaRepository<SavingsAccount,
@Query("SELECT sa.id FROM SavingsAccount sa WHERE sa.externalId = :externalId")
Long findIdByExternalId(@Param("externalId") ExternalId externalId);
@Query("""
SELECT new org.apache.fineract.portfolio.savings.data.SavingsAccrualData(
savings.id,
savings.accountNumber,
savings.accruedTillDate,
CASE WHEN apm.financialAccountType = 18 THEN TRUE ELSE FALSE END,
msp.allowOverdraft,
savings.depositType
)
FROM SavingsAccount savings
LEFT JOIN SavingsProduct msp ON msp = savings.product
LEFT JOIN ProductToGLAccountMapping apm ON apm.productId = msp.id and (apm.financialAccountType = 18 or apm.financialAccountType IS NULL)
WHERE savings.status = :status
AND (savings.nominalAnnualInterestRate IS NOT NULL AND savings.nominalAnnualInterestRate > 0)
AND msp.accountingRule = :accountingRule
AND ( savings.closedOnDate <= :tillDate OR savings.closedOnDate IS NULL)
AND ( savings.accruedTillDate <= :tillDate OR savings.accruedTillDate IS NULL )
ORDER BY savings.id
""")
List<SavingsAccrualData> findAccrualData(@Param("tillDate") LocalDate tillDate, @Param("savingsId") Long savingsId,
@Param("status") Integer status, @Param("accountingRule") Integer accountingRule);
}
@@ -22,6 +22,7 @@ import java.time.LocalDate;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.data.SavingsAccrualData;
import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
@@ -179,4 +180,10 @@ public class SavingsAccountRepositoryWrapper {
public Long findIdByExternalId(final ExternalId externalId) {
return this.repository.findIdByExternalId(externalId);
}
@Transactional(readOnly = true)
public List<SavingsAccrualData> findAccrualData(final LocalDate tillDate, final Long savingsId, final Integer status,
final Integer accountingRule) {
return this.repository.findAccrualData(tillDate, savingsId, status, accountingRule);
}
}
@@ -198,6 +198,14 @@ public final class SavingsAccountTransaction extends AbstractAuditableWithUTCDat
date, amount, isReversed, isManualTransaction, lienTransaction, refNo);
}
public static SavingsAccountTransaction accrual(final SavingsAccount savingsAccount, final Office office, final LocalDate date,
final Money amount, final boolean isManualTransaction, final String refNo) {
final boolean isReversed = false;
final Boolean lienTransaction = false;
return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.ACCRUAL.getValue(), date, amount,
isReversed, isManualTransaction, lienTransaction, refNo);
}
public static SavingsAccountTransaction interestPosting(final SavingsAccount savingsAccount, final Office office, final LocalDate date,
final Money amount, final boolean isManualTransaction) {
final boolean isReversed = false;
@@ -415,7 +423,7 @@ public final class SavingsAccountTransaction extends AbstractAuditableWithUTCDat
return Money.of(currency, this.overdraftAmount);
}
void setOverdraftAmount(Money overdraftAmount) {
public void setOverdraftAmount(Money overdraftAmount) {
this.overdraftAmount = overdraftAmount == null ? null : overdraftAmount.getAmount();
}
@@ -511,6 +519,10 @@ public final class SavingsAccountTransaction extends AbstractAuditableWithUTCDat
return this.isDeposit() || this.isWithdrawal() || this.isChargeTransaction() || this.isDividendPayout() || this.isInterestPosting();
}
public boolean isAccrual() {
return getTransactionType().isAccrual();
}
public boolean isInterestPostingAndNotReversed() {
return getTransactionType().isInterestPosting() && isNotReversed();
}
@@ -27,6 +27,8 @@ import org.apache.fineract.infrastructure.core.service.SearchParameters;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
import org.apache.fineract.portfolio.savings.data.SavingsAccrualData;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
public interface SavingsAccountReadPlatformService {
@@ -69,4 +71,6 @@ public interface SavingsAccountReadPlatformService {
List<SavingsAccountTransactionData> retrieveAllTransactionData(List<String> refNo);
Long retrieveAccountIdByExternalId(ExternalId externalId);
List<SavingsAccrualData> retrievePeriodicAccrualData(LocalDate tillDate, SavingsAccount savings);
}
@@ -23,4 +23,7 @@
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">
<!-- Sequence is starting from 2000 to make it easier to move existing liquibase changesets here -->
<include file="parts/2001_add_savings_accrual_job.xml" relativeToChangelogFile="true" />
<include file="parts/2002_add_savings_accrual_permission.xml" relativeToChangelogFile="true" />
<include file="parts/2003_add_accrued_till_date_to_savings_account.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,46 @@
<?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">
<insert tableName="job">
<column name="name" value="Add Accrual Transactions For Savings"/>
<column name="display_name" value="Add Accrual Transactions For Savings"/>
<column name="cron_expression" value="0 1 0 1/1 * ? *"/>
<column name="create_time" valueDate="${current_datetime}"/>
<column name="task_priority" valueNumeric="5"/>
<column name="group_name"/>
<column name="previous_run_start_time"/>
<column name="job_key" value="Add Accrual Transactions For Savings _ DEFAULT"/>
<column name="initializing_errorlog"/>
<column name="is_active" valueBoolean="false"/>
<column name="currently_running" valueBoolean="false"/>
<column name="updates_allowed" valueBoolean="true"/>
<column name="scheduler_group" valueNumeric="0"/>
<column name="is_misfired" valueBoolean="false"/>
<column name="node_id" valueNumeric="1"/>
<column name="is_mismatched_job" valueBoolean="true"/>
<column name="short_name" value="ADD_ATFS"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,34 @@
<?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">
<insert tableName="m_permission">
<column name="grouping" value="accounting"/>
<column name="code" value="EXECUTEFORSAVINGS"/>
<column name="entity_name" value="PERIODICACCRUALACCOUNTINGFORSAVINGS"/>
<column name="action_name" value="EXECUTE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,31 @@
<?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">
<addColumn tableName="m_savings_account">
<column name="accrued_till_date" type="DATE" defaultValueComputed="NULL" />
</addColumn>
</changeSet>
</databaseChangeLog>