From 352ef6317c20c8d7d8ae2c382a2a33b1c5bc83ed Mon Sep 17 00:00:00 2001 From: Ahmed Adel Date: Tue, 25 Nov 2025 18:22:25 +0200 Subject: [PATCH] FINERACT-2326: Refactor trial balance processing to reduce cognitive complexity --- .../domain/TrialBalanceRepository.java | 31 +++++ .../UpdateTrialBalanceDetailsConfig.java | 7 +- .../UpdateTrialBalanceDetailsTasklet.java | 128 +++++++++++------- .../domain/JournalEntryRepository.java | 24 ++++ 4 files changed, 140 insertions(+), 50 deletions(-) diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/glaccount/domain/TrialBalanceRepository.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/glaccount/domain/TrialBalanceRepository.java index f49e8b5d4..dd83fc4f6 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/glaccount/domain/TrialBalanceRepository.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/glaccount/domain/TrialBalanceRepository.java @@ -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 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 findLastClosingBalance(Long officeId, Long accountId); + + @Query(""" + SELECT DISTINCT tb.officeId + FROM TrialBalance tb + WHERE tb.closingBalance IS NULL + """) + List findDistinctOfficeIdsWithNullClosingBalance(); + + @Query(""" + SELECT DISTINCT tb.glAccountId + FROM TrialBalance tb + WHERE tb.officeId = :officeId + AND tb.closingBalance IS NULL + """) + List findDistinctAccountIdsWithNullClosingBalanceByOfficeId(Long officeId); + } diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/glaccount/jobs/updatetrialbalancedetails/UpdateTrialBalanceDetailsConfig.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/glaccount/jobs/updatetrialbalancedetails/UpdateTrialBalanceDetailsConfig.java index 3b764c3e7..cb327cfb1 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/glaccount/jobs/updatetrialbalancedetails/UpdateTrialBalanceDetailsConfig.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/glaccount/jobs/updatetrialbalancedetails/UpdateTrialBalanceDetailsConfig.java @@ -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); } } diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/glaccount/jobs/updatetrialbalancedetails/UpdateTrialBalanceDetailsTasklet.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/glaccount/jobs/updatetrialbalancedetails/UpdateTrialBalanceDetailsTasklet.java index a68e431e0..e4718f3f5 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/glaccount/jobs/updatetrialbalancedetails/UpdateTrialBalanceDetailsTasklet.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/glaccount/jobs/updatetrialbalancedetails/UpdateTrialBalanceDetailsTasklet.java @@ -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 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 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 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 closingBalanceData = jdbcTemplate.queryForList(closingBalanceQuery, BigDecimal.class, officeId, accountId); - List 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 tbGaps = journalEntryRepository.findTransactionDatesAfter(baselineDate); + for (LocalDate tbGap : tbGaps) { + if (DateUtils.getExactDifferenceInDays(tbGap, DateUtils.getBusinessLocalDate()) < 1) { + continue; + } + insertTrialBalanceForDate(tbGap); + } + } + + private void insertTrialBalanceForDate(LocalDate tbGap) { + List rows = journalEntryRepository.findTrialBalanceLinesForDate(tbGap); + + List 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 officeIds = trialBalanceRepository.findDistinctOfficeIdsWithNullClosingBalance(); + + for (Long officeId : officeIds) { + updateClosingBalancesForOffice(jdbcTemplate, officeId); + } + } + + private void updateClosingBalancesForOffice(JdbcTemplate jdbcTemplate, Long officeId) { + final List 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 tbRows = trialBalanceRepositoryWrapper.findNewByOfficeAndAccount(officeId, accountId); + + updateTrialBalanceRows(tbRows, closingBalance); + } + + private BigDecimal getPreviousClosingBalance(Long officeId, Long accountId) { + List closingBalanceData = trialBalanceRepository.findLastClosingBalance(officeId, accountId); + return CollectionUtils.isEmpty(closingBalanceData) ? BigDecimal.ZERO : closingBalanceData.getFirst(); + } + + private void updateTrialBalanceRows(List tbRows, BigDecimal initialClosingBalance) { + BigDecimal closingBalance = initialClosingBalance; + + for (TrialBalance row : tbRows) { + if (closingBalance != null) { + closingBalance = closingBalance.add(row.getAmount()); + } + row.setClosingBalance(closingBalance); + } + } } diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntryRepository.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntryRepository.java index b69c01803..b078042ab 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntryRepository.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntryRepository.java @@ -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 findJournalEntries(@Param("transactionId") String transactionId, @Param("entityType") Integer entityType); + @Query(""" + SELECT DISTINCT je.transactionDate + FROM JournalEntry je + WHERE je.transactionDate>:afterDate + """) + List 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 findTrialBalanceLinesForDate(@Param("transactionDate") LocalDate transactionDate); + }