FINERACT-2326: Refactor trial balance processing to reduce cognitive complexity

This commit is contained in:
Ahmed Adel
2025-12-16 12:35:06 +01:00
committed by Adam Saghy
parent e5be162117
commit 352ef6317c
4 changed files with 140 additions and 50 deletions
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.accounting.glaccount.domain;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -28,4 +30,33 @@ public interface TrialBalanceRepository extends JpaRepository<TrialBalance, Long
@Query(value = "select * from m_trial_balance where office_id=:officeId and account_id=:accountId and closing_balance is null order by created_date, entry_date", nativeQuery = true)
List<TrialBalance> findNewByOfficeAndAccount(@Param("officeId") Long officeId, @Param("accountId") Long accountId);
@Query("SELECT MAX(tb.transactionDate) FROM TrialBalance tb")
LocalDate findMaxCreatedDate();
@Query("""
SELECT tb.closingBalance
FROM TrialBalance tb
WHERE tb.officeId = :officeId
AND tb.glAccountId = :accountId
AND tb.closingBalance IS NOT NULL
ORDER BY tb.transactionDate DESC, tb.entryDate DESC
""")
List<BigDecimal> findLastClosingBalance(Long officeId, Long accountId);
@Query("""
SELECT DISTINCT tb.officeId
FROM TrialBalance tb
WHERE tb.closingBalance IS NULL
""")
List<Long> findDistinctOfficeIdsWithNullClosingBalance();
@Query("""
SELECT DISTINCT tb.glAccountId
FROM TrialBalance tb
WHERE tb.officeId = :officeId
AND tb.closingBalance IS NULL
""")
List<Long> findDistinctAccountIdsWithNullClosingBalanceByOfficeId(Long officeId);
}
@@ -19,7 +19,9 @@
package org.apache.fineract.accounting.glaccount.jobs.updatetrialbalancedetails;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.glaccount.domain.TrialBalanceRepository;
import org.apache.fineract.accounting.glaccount.domain.TrialBalanceRepositoryWrapper;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSourceServiceFactory;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.springframework.batch.core.Job;
@@ -40,6 +42,8 @@ public class UpdateTrialBalanceDetailsConfig {
private final PlatformTransactionManager transactionManager;
private final RoutingDataSourceServiceFactory dataSourceServiceFactory;
private final TrialBalanceRepositoryWrapper trialBalanceRepositoryWrapper;
private final TrialBalanceRepository trialBalanceRepository;
private final JournalEntryRepository journalEntryRepository;
@Bean
protected Step updateTrialBalanceDetailsStep() {
@@ -55,6 +59,7 @@ public class UpdateTrialBalanceDetailsConfig {
@Bean
public UpdateTrialBalanceDetailsTasklet updateTrialBalanceDetailsTasklet() {
return new UpdateTrialBalanceDetailsTasklet(dataSourceServiceFactory, trialBalanceRepositoryWrapper);
return new UpdateTrialBalanceDetailsTasklet(dataSourceServiceFactory, trialBalanceRepositoryWrapper, trialBalanceRepository,
journalEntryRepository);
}
}
@@ -24,7 +24,9 @@ import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.accounting.glaccount.domain.TrialBalance;
import org.apache.fineract.accounting.glaccount.domain.TrialBalanceRepository;
import org.apache.fineract.accounting.glaccount.domain.TrialBalanceRepositoryWrapper;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSourceServiceFactory;
@@ -41,59 +43,87 @@ public class UpdateTrialBalanceDetailsTasklet implements Tasklet {
private final RoutingDataSourceServiceFactory dataSourceServiceFactory;
private final TrialBalanceRepositoryWrapper trialBalanceRepositoryWrapper;
private final TrialBalanceRepository trialBalanceRepository;
private final JournalEntryRepository journalEntryRepository;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
final JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourceServiceFactory.determineDataSourceService().retrieveDataSource());
final StringBuilder tbGapSqlBuilder = new StringBuilder(500);
tbGapSqlBuilder.append("select distinct(je.transaction_date) ").append("from acc_gl_journal_entry je ")
.append("where je.transaction_date > (select coalesce(MAX(created_date),'2010-01-01') from m_trial_balance)");
final List<LocalDate> tbGaps = jdbcTemplate.queryForList(tbGapSqlBuilder.toString(), LocalDate.class);
for (LocalDate tbGap : tbGaps) {
int days = DateUtils.getExactDifferenceInDays(tbGap, DateUtils.getBusinessLocalDate());
if (days < 1) {
continue;
}
final StringBuilder sqlBuilder = new StringBuilder(600);
sqlBuilder.append("Insert Into m_trial_balance(office_id, account_id, Amount, entry_date, created_date,closing_balance) ")
.append("Select je.office_id, je.account_id, SUM(CASE WHEN je.type_enum=1 THEN (-1) * je.amount ELSE je.amount END) ")
.append("as Amount, Date(je.entry_date) as Entry_Date, je.transaction_date as Created_Date,sum(je.amount) as closing_balance ")
.append("from acc_gl_journal_entry je WHERE je.transaction_date = ? ")
.append("group by je.account_id, je.office_id, je.transaction_date, Date(je.entry_date)");
final int result = jdbcTemplate.update(sqlBuilder.toString(), tbGap);
log.debug("{}: Records affected by updateTrialBalanceDetails: {}", ThreadLocalContextUtil.getTenant().getName(), result);
}
String distinctOfficeQuery = "select distinct(office_id) from m_trial_balance where closing_balance is null group by office_id";
final List<Long> officeIds = jdbcTemplate.queryForList(distinctOfficeQuery, Long.class);
for (Long officeId : officeIds) {
String distinctAccountQuery = "select distinct(account_id) from m_trial_balance where office_id=? and closing_balance is null group by account_id";
final List<Long> accountIds = jdbcTemplate.queryForList(distinctAccountQuery, Long.class, officeId);
for (Long accountId : accountIds) {
final String closingBalanceQuery = "select closing_balance from m_trial_balance where office_id=? and account_id=? and closing_balance "
+ "is not null order by created_date desc, entry_date desc limit 1";
List<BigDecimal> closingBalanceData = jdbcTemplate.queryForList(closingBalanceQuery, BigDecimal.class, officeId, accountId);
List<TrialBalance> tbRows = trialBalanceRepositoryWrapper.findNewByOfficeAndAccount(officeId, accountId);
BigDecimal closingBalance = null;
if (!CollectionUtils.isEmpty(closingBalanceData)) {
closingBalance = closingBalanceData.get(0);
}
if (CollectionUtils.isEmpty(closingBalanceData)) {
closingBalance = BigDecimal.ZERO;
for (TrialBalance row : tbRows) {
closingBalance = closingBalance.add(row.getAmount());
row.setClosingBalance(closingBalance);
}
} else {
for (TrialBalance tbRow : tbRows) {
if (closingBalance != null) {
closingBalance = closingBalance.add(tbRow.getAmount());
}
tbRow.setClosingBalance(closingBalance);
}
}
trialBalanceRepositoryWrapper.save(tbRows);
}
}
processTrialBalanceGaps(jdbcTemplate);
updateClosingBalances(jdbcTemplate);
return RepeatStatus.FINISHED;
}
private void processTrialBalanceGaps(JdbcTemplate jdbcTemplate) {
LocalDate maxCreatedDate = trialBalanceRepository.findMaxCreatedDate();
LocalDate baselineDate = maxCreatedDate != null ? maxCreatedDate : LocalDate.of(2010, 1, 1);
List<LocalDate> tbGaps = journalEntryRepository.findTransactionDatesAfter(baselineDate);
for (LocalDate tbGap : tbGaps) {
if (DateUtils.getExactDifferenceInDays(tbGap, DateUtils.getBusinessLocalDate()) < 1) {
continue;
}
insertTrialBalanceForDate(tbGap);
}
}
private void insertTrialBalanceForDate(LocalDate tbGap) {
List<Object[]> rows = journalEntryRepository.findTrialBalanceLinesForDate(tbGap);
List<TrialBalance> trialBalances = rows.stream().map(row -> {
TrialBalance tb = new TrialBalance();
tb.setOfficeId((Long) row[0]);
tb.setGlAccountId((Long) row[1]);
tb.setAmount((BigDecimal) row[2]);
tb.setEntryDate((LocalDate) row[3]);
tb.setTransactionDate((LocalDate) row[4]);
tb.setClosingBalance((BigDecimal) row[5]);
return tb;
}).toList();
trialBalanceRepositoryWrapper.save(trialBalances);
log.debug("{}: Records affected by updateTrialBalanceDetails: {}", ThreadLocalContextUtil.getTenant().getName(),
trialBalances.size());
}
private void updateClosingBalances(JdbcTemplate jdbcTemplate) {
final List<Long> officeIds = trialBalanceRepository.findDistinctOfficeIdsWithNullClosingBalance();
for (Long officeId : officeIds) {
updateClosingBalancesForOffice(jdbcTemplate, officeId);
}
}
private void updateClosingBalancesForOffice(JdbcTemplate jdbcTemplate, Long officeId) {
final List<Long> accountIds = trialBalanceRepository.findDistinctAccountIdsWithNullClosingBalanceByOfficeId(officeId);
for (Long accountId : accountIds) {
updateClosingBalanceForAccount(jdbcTemplate, officeId, accountId);
}
}
private void updateClosingBalanceForAccount(JdbcTemplate jdbcTemplate, Long officeId, Long accountId) {
BigDecimal closingBalance = getPreviousClosingBalance(officeId, accountId);
List<TrialBalance> tbRows = trialBalanceRepositoryWrapper.findNewByOfficeAndAccount(officeId, accountId);
updateTrialBalanceRows(tbRows, closingBalance);
}
private BigDecimal getPreviousClosingBalance(Long officeId, Long accountId) {
List<BigDecimal> closingBalanceData = trialBalanceRepository.findLastClosingBalance(officeId, accountId);
return CollectionUtils.isEmpty(closingBalanceData) ? BigDecimal.ZERO : closingBalanceData.getFirst();
}
private void updateTrialBalanceRows(List<TrialBalance> tbRows, BigDecimal initialClosingBalance) {
BigDecimal closingBalance = initialClosingBalance;
for (TrialBalance row : tbRows) {
if (closingBalance != null) {
closingBalance = closingBalance.add(row.getAmount());
}
row.setClosingBalance(closingBalance);
}
}
}
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.accounting.journalentry.domain;
import java.time.LocalDate;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -41,4 +42,27 @@ public interface JournalEntryRepository extends JpaRepository<JournalEntry, Long
@Query("select journalEntry from JournalEntry journalEntry where journalEntry.transactionId= :transactionId and journalEntry.reversed=false and journalEntry.entityType = :entityType order by journalEntry.transactionDate asc, journalEntry.createdDate asc, journalEntry.id asc")
List<JournalEntry> findJournalEntries(@Param("transactionId") String transactionId, @Param("entityType") Integer entityType);
@Query("""
SELECT DISTINCT je.transactionDate
FROM JournalEntry je
WHERE je.transactionDate>:afterDate
""")
List<LocalDate> findTransactionDatesAfter(LocalDate afterDate);
@Query("""
SELECT je.office.id,
je.glAccount.id,
SUM(CASE WHEN je.type = 1
THEN -1 * je.amount
ELSE je.amount
END),
je.transactionDate,
je.createdDate,
SUM(je.amount)
FROM JournalEntry je
WHERE je.transactionDate = :transactionDate
GROUP BY je.office.id, je.glAccount.id, je.transactionDate, je.createdDate
""")
List<Object[]> findTrialBalanceLinesForDate(@Param("transactionDate") LocalDate transactionDate);
}