diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java index d8ae49255..41cd2bf0b 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java @@ -33,7 +33,6 @@ public final class GlobalConfigurationConstants { public static final String GRACE_ON_PENALTY_POSTING = "grace-on-penalty-posting"; public static final String SAVINGS_INTEREST_POSTING_CURRENT_PERIOD_END = "savings-interest-posting-current-period-end"; public static final String FD_INTEREST_POSTING_APPROVAL = "fd-interest-posting-approval"; - public static final String FD_MATURITY_INTEREST_POSTING_APPROVAL = "fd-maturity-interest-posting-approval"; public static final String FINANCIAL_YEAR_BEGINNING_MONTH = "financial-year-beginning-month"; public static final String MIN_CLIENTS_IN_GROUP = "min-clients-in-group"; public static final String MAX_CLIENTS_IN_GROUP = "max-clients-in-group"; diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java index 4141516ed..fb0c03c76 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java @@ -75,13 +75,6 @@ public interface ConfigurationDomainService { */ boolean isFixedDepositInterestPostingApprovalEnabled(); - /** - * Whether a fixed deposit maturing is itself reported and approved before its final interest posts - * and its closure (or renewal) instructions run, instead of all three happening directly, the - * moment it matures. - */ - boolean isFixedDepositMaturityInterestPostingApprovalEnabled(); - Integer retrieveFinancialYearBeginningMonth(); Integer retrieveMinAllowedClientsInGroup(); diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java index 7828413bb..b442ca1af 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java @@ -45,7 +45,6 @@ public enum JobName { SAVINGS_DAILY_INTEREST("Savings Daily Interest Accrual And Posting"), // SAVINGS_INTEREST_POSTING_REPORT("Generate Savings Interest Posting Report"), // FD_INTEREST_POSTING_REPORT("Generate Fixed Deposit Interest Posting Report"), // - FD_MATURITY_REPORT("Generate Fixed Deposit Maturity Report"), // ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_LOANS_WITH_INCOME_POSTED_AS_TRANSACTIONS( "Add Accrual Transactions For Loans With Income Posted As Transactions"), // EXECUTE_REPORT_MAILING_JOBS("Execute Report Mailing Jobs"), // diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index 5aa32f27e..2b5f99d3b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -201,11 +201,6 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService return getGlobalConfigurationPropertyData(GlobalConfigurationConstants.FD_INTEREST_POSTING_APPROVAL).isEnabled(); } - @Override - public boolean isFixedDepositMaturityInterestPostingApprovalEnabled() { - return getGlobalConfigurationPropertyData(GlobalConfigurationConstants.FD_MATURITY_INTEREST_POSTING_APPROVAL).isEnabled(); - } - @Override public Integer retrieveFinancialYearBeginningMonth() { final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData( diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java index c2e3f4018..79720f616 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java @@ -50,8 +50,6 @@ import org.apache.fineract.portfolio.savings.dailyinterest.SavingsDailyInterestO import org.apache.fineract.portfolio.savings.dailyinterest.SavingsInterestPostingReportService; import org.apache.fineract.portfolio.savings.fdinterest.FixedDepositInterestPostingOrchestrator; import org.apache.fineract.portfolio.savings.fdinterest.FixedDepositInterestPostingReportService; -import org.apache.fineract.portfolio.savings.fdmaturity.FixedDepositMaturityOrchestrator; -import org.apache.fineract.portfolio.savings.fdmaturity.FixedDepositMaturityReportService; import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @@ -97,8 +95,6 @@ public class BranchDayRegisterApiResource { private final SavingsDailyInterestOrchestrator savingsDailyInterestOrchestrator; private final FixedDepositInterestPostingReportService fdInterestPostingReportService; private final FixedDepositInterestPostingOrchestrator fdInterestPostingOrchestrator; - private final FixedDepositMaturityReportService fdMaturityReportService; - private final FixedDepositMaturityOrchestrator fdMaturityOrchestrator; private final Gson gson = new Gson(); // ── GET /v1/branchday ── the register itself ────────────────────────────── @@ -249,11 +245,6 @@ public class BranchDayRegisterApiResource { final Map fdInterest = fdInterestPostingReportService.dayCloseInterestView(officeId, businessDate); result.put("fdInterestPending", fdInterest.get("pending")); result.put("fdInterestDue", fdInterest.get("due")); - // Fixed deposits maturing today — the same read, now for the event of maturing rather than a - // periodic posting, blocking the close on the same grounds. - final Map fdMaturity = fdMaturityReportService.dayCloseMaturityView(officeId, businessDate); - result.put("fdMaturityPending", fdMaturity.get("pending")); - result.put("fdMaturityDue", fdMaturity.get("due")); return gson.toJson(result); } @@ -644,17 +635,6 @@ public class BranchDayRegisterApiResource { + ". Approve it under Reports → FD Interest Posting Reports, then close the day."); } - // ── And its fixed deposit maturity reports, on the same grounds ── - // Only while maturities are on the approval path; with that switched off the nightly job - // settles a matured deposit directly and there is nothing here to approve. - final List fdMaturityPending = fdMaturityReportService.reportsBlockingDayClose(officeId, businessDate); - if (!fdMaturityPending.isEmpty()) { - throw new PlatformDataIntegrityException("error.msg.branchday.fd.maturity.report.pending", - "Cannot close the day — this branch's fixed deposit maturity report is not approved for: " - + String.join(", ", fdMaturityPending) - + ". Approve it under Reports → FD Maturity Reports, then close the day."); - } - // ── The head office closes last ── // Every branch that opened the day closes first and posts its own interest; a branch that // never opened is not waited for. @@ -807,25 +787,6 @@ public class BranchDayRegisterApiResource { } } - // ── And the branch's approved fixed deposit maturities ── - // Each approved deposit's status, interest and closure all run together here, in the real - // DepositAccountWritePlatformService#updateMaturityDetails — this only decides that the moment - // has come. - final List fdMaturityToPost = new ArrayList<>(fdMaturityReportService.approvedReportIds(officeId, businessDate)); - if (isHeadOffice) { - fdMaturityToPost.addAll(fdMaturityReportService.approvedReportIdsOfBranchesNotOpened(businessDate, officeId)); - } - if (!fdMaturityToPost.isEmpty()) { - log.info("Day close {}: office {} posting {} approved FD maturity report(s)", businessDate, officeId, - fdMaturityToPost.size()); - final List fdMaturityFailures = fdMaturityOrchestrator.postReports(fdMaturityToPost); - if (!fdMaturityFailures.isEmpty()) { - throw new PlatformDataIntegrityException("error.msg.branchday.fd.maturity.posting.failed", - "Cannot close the day — fixed deposit maturities could not be posted: " + String.join("; ", fdMaturityFailures) - + ". The report is still approved; fix the cause and close the day again."); - } - } - // "closed_at IS NULL" in the WHERE, not just the read above: the already-closed check and // this write are two separate statements, so two managers pressing Close at once (or one // double-click) both got past the check. The second UPDATE then overwrote the first's diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java index 58f928283..5226ede94 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java @@ -719,38 +719,6 @@ public class FixedDepositAccount extends SavingsAccount { this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); } - /** - * What {@link #postMaturityInterest} would credit and has not been paid yet — worked out the exact - * same way, down to the same period-already-paid check, but nothing is added to the account. A - * maturity interest report asks this before anyone has approved anything, so the account must come - * out of the call exactly as it went in; the three-way correction branch {@code postMaturityInterest} - * takes for a stale posted amount has no analogue here; a period already posted even if the figure - * since changed is simply not unpaid. - */ - public List previewMaturityInterestPeriods(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, - final Integer financialYearBeginningMonth) { - final LocalDate interestPostingUpToDate = maturityDate(); - final MathContext mc = MathContext.DECIMAL64; - final boolean isInterestTransfer = false; - final LocalDate postInterestOnDate = null; - final boolean backdatedTxnsAllowedTill = false; - final boolean postReversals = false; - final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, - postReversals); - - final List unpaid = new ArrayList<>(); - for (final PostingPeriod period : postingPeriods) { - LocalDate transactionDate = period.dateOfPostingTransaction(); - transactionDate = DateUtils.isAfter(transactionDate, interestPostingUpToDate) ? interestPostingUpToDate : transactionDate; - final SavingsAccountTransaction existing = findInterestPostingTransactionFor(transactionDate); - if (existing == null || !interestAlreadyPaidOut(existing)) { - unpaid.add(period); - } - } - return unpaid; - } - /** * Whether this period's posted interest has already left the account for the linked savings account. *

diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdmaturity/FixedDepositMaturityOrchestrator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdmaturity/FixedDepositMaturityOrchestrator.java deleted file mode 100644 index bf4ea896c..000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdmaturity/FixedDepositMaturityOrchestrator.java +++ /dev/null @@ -1,163 +0,0 @@ -/** - * 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.fdmaturity; - -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.portfolio.savings.DepositAccountType; -import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Service; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.support.TransactionTemplate; - -/** - * Posts approved fixed deposit maturity reports. - * - *

- * Posting one deposit's maturity is calling the real, unchanged - * {@link DepositAccountWritePlatformService#updateMaturityDetails} — status, interest and closure all - * run together exactly as they always have. This orchestrator's own job is only deciding when: each - * deposit runs in a transaction of its own, so one with a bad closure target does not undo the others - * that matured cleanly, and a line already posted (by an earlier, partly-failed attempt) is skipped — - * retrying a report only touches what did not go through last time. A report with any deposit still - * unposted stays APPROVED rather than POSTED, so closing again retries exactly those. - */ -@Service -@Slf4j -@RequiredArgsConstructor -public class FixedDepositMaturityOrchestrator { - - private static final String APPROVED = "APPROVED"; - - private final JdbcTemplate jdbcTemplate; - private final DepositAccountWritePlatformService depositAccountWritePlatformService; - private final FixedDepositMaturityReportService reportService; - private final PlatformTransactionManager transactionManager; - - /** - * Posts each report's still-unposted deposits. - * - * @return one message per report left with a deposit that could not mature, empty where every - * report fully posted - */ - public List postReports(final List reportIds) { - final TransactionTemplate newTransaction = new TransactionTemplate(this.transactionManager); - newTransaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - - final List failures = new ArrayList<>(); - for (final Long reportId : reportIds) { - final String failure = postReport(reportId, newTransaction); - if (failure != null) { - failures.add(failure); - } - } - return failures; - } - - /** @return a message for this report if it has a deposit left unposted, null where it fully posted */ - private String postReport(final Long reportId, final TransactionTemplate newTransaction) { - final Map report = this.jdbcTemplate.queryForMap( - "SELECT office_id, posting_date, status FROM m_fd_maturity_report WHERE id = ?", reportId); - if (!APPROVED.equals(report.get("status"))) { - log.warn("FD maturity: report {} is {}, not APPROVED — skipped", reportId, report.get("status")); - return null; - } - final long officeId = ((Number) report.get("office_id")).longValue(); - final LocalDate postingDate = ((java.sql.Date) report.get("posting_date")).toLocalDate(); - - final List> lines = this.jdbcTemplate.queryForList(""" - SELECT id, fd_account_id, account_no FROM m_fd_maturity_report_line WHERE report_id = ? AND posted_at IS NULL - """, reportId); - - final long runId = newTransaction.execute(status -> openRun(reportId, officeId, postingDate, lines.size())); - - int posted = 0; - final List lineFailures = new ArrayList<>(); - for (final Map line : lines) { - final long lineId = ((Number) line.get("id")).longValue(); - final long accountId = ((Number) line.get("fd_account_id")).longValue(); - final String accountNo = (String) line.get("account_no"); - try { - newTransaction.executeWithoutResult(status -> { - this.depositAccountWritePlatformService.updateMaturityDetails(accountId, DepositAccountType.FIXED_DEPOSIT); - this.jdbcTemplate.update( - "UPDATE m_fd_maturity_report_line SET posted_at = CURRENT_TIMESTAMP, failure_reason = NULL WHERE id = ?", - lineId); - }); - posted++; - } catch (final RuntimeException e) { - log.error("FD maturity: deposit {} (line {} of report {}) could not mature", accountNo, lineId, reportId, e); - final String reason = truncate(e.getMessage()); - newTransaction.executeWithoutResult( - status -> this.jdbcTemplate.update("UPDATE m_fd_maturity_report_line SET failure_reason = ? WHERE id = ?", - reason, lineId)); - lineFailures.add(accountNo + ": " + reason); - } - } - - final int posted2 = posted; - if (lineFailures.isEmpty()) { - newTransaction.executeWithoutResult(status -> completeRun(runId, posted2, "COMPLETED", null)); - newTransaction.executeWithoutResult(status -> this.jdbcTemplate.update(""" - UPDATE m_fd_maturity_report SET status = 'POSTED', posted_at = CURRENT_TIMESTAMP, posting_run_id = ?, - failure_reason = NULL - WHERE id = ? - """, runId, reportId)); - log.info("FD maturity: report {} (branch {}, {}) matured {} deposit(s)", reportId, officeId, postingDate, posted); - return null; - } - - final String summary = String.join("; ", lineFailures); - newTransaction.executeWithoutResult(status -> completeRun(runId, posted2, "FAILED", summary)); - this.reportService.recordPostingFailure(reportId, summary); - return "report " + reportId + " (branch " + officeId + "): " + lineFailures.size() + " of " + lines.size() - + " deposit(s) could not mature — " + summary; - } - - private long openRun(final long reportId, final long officeId, final LocalDate postingDate, final int accountCount) { - this.jdbcTemplate.update(""" - INSERT INTO m_fd_maturity_run (report_id, office_id, posting_date, account_count, status) - VALUES (?, ?, ?, ?, 'PENDING') - ON CONFLICT (report_id) DO UPDATE SET - account_count = EXCLUDED.account_count, status = 'PENDING', started_at = CURRENT_TIMESTAMP, failure_reason = NULL - """, reportId, officeId, postingDate, accountCount); - return this.jdbcTemplate.queryForObject("SELECT id FROM m_fd_maturity_run WHERE report_id = ?", Long.class, reportId); - } - - private void completeRun(final long runId, final int postedCount, final String status, final String failureReason) { - this.jdbcTemplate.update(""" - UPDATE m_fd_maturity_run - SET posted_count = ?, status = ?, failure_reason = ?, completed_at = CURRENT_TIMESTAMP - WHERE id = ? - """, postedCount, status, failureReason, runId); - } - - private static String truncate(final String reason) { - if (reason == null) { - return "unknown failure"; - } - return reason.length() > 1000 ? reason.substring(0, 1000) : reason; - } -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdmaturity/FixedDepositMaturityReportService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdmaturity/FixedDepositMaturityReportService.java deleted file mode 100644 index e1aa10ca6..000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdmaturity/FixedDepositMaturityReportService.java +++ /dev/null @@ -1,691 +0,0 @@ -/** - * 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.fdmaturity; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; -import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.portfolio.savings.DepositAccountType; -import org.apache.fineract.portfolio.savings.dailyinterest.SavingsInterestPostingReportService; -import org.apache.fineract.portfolio.savings.domain.DepositAccountAssembler; -import org.apache.fineract.portfolio.savings.domain.FixedDepositAccount; -import org.apache.fineract.portfolio.savings.domain.interest.PostingPeriod; -import org.apache.fineract.useradministration.domain.AppUser; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Service; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.support.TransactionTemplate; - -/** - * The fixed deposit maturity report: every deposit of one branch maturing on a date with no closure - * instruction of its own, what its last posting would still come to, put in front of the branch before - * any of that happens. - * - *

- * Only a deposit with nothing decided for it needs this: one that already named Transfer to Savings or - * a Reinvest option on its own closure instruction settles itself the moment it matures, exactly as it - * always has — the approval question is for a decision nobody has made yet, not one the customer - * already made in writing when the account was opened. See {@code autoMatured} for visibility into - * those. For the undecided ones, this report puts the same two-eyes gate 0593 put in front of periodic - * interest in front of the maturity event itself: one user generates it, a different user approves it, - * and only on approval does the real maturity job — - * {@link org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService#updateMaturityDetails} - * — actually run for that account, as the branch closes its day. Until then the account stays ACTIVE; - * status, interest and whatever renewal is later chosen from the FD Dashboard all change together in - * that one real run, never in stages. - * - *

- * The estimated interest a line shows is read with - * {@link FixedDepositAccount#previewMaturityInterestPeriods}, the exact same calculation the real - * posting uses, just without writing anything down — so a figure approved here is never different - * from what the real run later credits unless the account's own transactions changed in between, the - * same risk the periodic report already lives with. - */ -@Service -@Slf4j -public class FixedDepositMaturityReportService { - - public static final String GENERATED = "GENERATED"; - public static final String APPROVED = "APPROVED"; - public static final String POSTED = "POSTED"; - public static final String SUPERSEDED = "SUPERSEDED"; - - private static final int MONEY_SCALE = 2; - private static final RoundingMode MONEY_ROUNDING = RoundingMode.HALF_EVEN; - private static final int ACTIVE_STATUS = 300; - private static final int DEPOSIT_TYPE_FIXED = 200; - - private static final String CLOSURE_LABEL_CASE = """ - CASE COALESCE(datp.on_account_closure_enum, 0) - WHEN 100 THEN 'WITHDRAW_DEPOSIT' WHEN 200 THEN 'TRANSFER_TO_SAVINGS' - WHEN 300 THEN 'REINVEST_PRINCIPAL_AND_INTEREST' WHEN 400 THEN 'REINVEST_PRINCIPAL_ONLY' - WHEN 500 THEN 'PARTIAL_REINVEST' WHEN 600 THEN 'TRANSFER_TO_GL' ELSE 'NONE' - END - """; - - private final JdbcTemplate jdbcTemplate; - private final DepositAccountAssembler depositAccountAssembler; - private final ConfigurationDomainService configurationDomainService; - /** Borrowed for the branch rules, same reason the interest report borrows it. */ - private final SavingsInterestPostingReportService branchAuthority; - private final TransactionTemplate transactionTemplate; - /** - * Open-session-in-view is off (spring.jpa.open-in-view=false), so loading the account and reading - * its lazy associations has to happen inside one transaction of its own — a read called from a plain - * GET would otherwise load the account, close the session, and throw the moment - * calculateInterestUsing touched anything not already fetched. Read-only, which both documents the - * intent and is itself a second guarantee, alongside previewMaturityInterestPeriods's own, that - * nothing here is ever written down. - */ - private final TransactionTemplate readOnlyTransaction; - - public FixedDepositMaturityReportService(final JdbcTemplate jdbcTemplate, final DepositAccountAssembler depositAccountAssembler, - final ConfigurationDomainService configurationDomainService, final SavingsInterestPostingReportService branchAuthority, - final PlatformTransactionManager transactionManager) { - this.jdbcTemplate = jdbcTemplate; - this.depositAccountAssembler = depositAccountAssembler; - this.configurationDomainService = configurationDomainService; - this.branchAuthority = branchAuthority; - this.transactionTemplate = new TransactionTemplate(transactionManager); - this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - this.readOnlyTransaction = new TransactionTemplate(transactionManager); - this.readOnlyTransaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - this.readOnlyTransaction.setReadOnly(true); - } - - /** One deposit maturing on the report's date, priced but not yet posted. */ - public record DueLine(Long accountId, String accountNo, String clientName, Long officeId, Long productId, String productName, - String fdType, LocalDate maturityDate, BigDecimal depositAmount, BigDecimal interestRate, BigDecimal estimatedInterest, - String closureInstruction, String closureAccountNo) {} - - // ── computing what is due ──────────────────────────────────────────────────────────────────── - - /** - * Every active fixed deposit of the branch (or every branch, with officeId null) whose maturity - * date has arrived by the posting date and is not already on a live report for it. - */ - public List computeDue(final LocalDate postingDate, final Long officeId) { - final StringBuilder sql = new StringBuilder(""" - SELECT sa.id AS account_id, sa.account_no, sa.product_id, sp.name AS product_name, sp.fd_type, - COALESCE(c.display_name, g.display_name) AS client_name, - COALESCE(c.office_id, g.office_id) AS office_id, - sa.nominal_annual_interest_rate AS interest_rate, - datp.deposit_amount, datp.maturity_date, datp.transfer_to_savings_account_id, - ls.account_no AS closure_account_no, - """); - sql.append(CLOSURE_LABEL_CASE).append(" AS closure_instruction\n"); - sql.append(""" - FROM m_savings_account sa - JOIN m_savings_product sp ON sp.id = sa.product_id - JOIN m_deposit_account_term_and_preclosure datp ON datp.savings_account_id = sa.id - LEFT JOIN m_client c ON c.id = sa.client_id - LEFT JOIN m_group g ON g.id = sa.group_id - LEFT JOIN m_savings_account ls ON ls.id = datp.transfer_to_savings_account_id - WHERE sa.status_enum = ? AND sa.deposit_type_enum = ? AND datp.maturity_date <= ? - AND COALESCE(datp.on_account_closure_enum, 0) = 0 - AND NOT EXISTS (SELECT 1 FROM m_fd_maturity_report_line l - JOIN m_fd_maturity_report r ON r.id = l.report_id - WHERE l.fd_account_id = sa.id AND r.status <> 'SUPERSEDED') - """); - final List params = new ArrayList<>(List.of(ACTIVE_STATUS, DEPOSIT_TYPE_FIXED, postingDate)); - if (officeId != null) { - sql.append(" AND COALESCE(c.office_id, g.office_id) = ?"); - params.add(officeId); - } - sql.append(" ORDER BY sa.account_no"); - - final List> rows = this.jdbcTemplate.query(sql.toString(), (rs, rowNum) -> { - final Map row = new LinkedHashMap<>(); - row.put("account_id", rs.getLong("account_id")); - row.put("account_no", rs.getString("account_no")); - row.put("product_id", rs.getLong("product_id")); - row.put("product_name", rs.getString("product_name")); - row.put("fd_type", rs.getString("fd_type")); - row.put("client_name", rs.getString("client_name")); - row.put("office_id", (Long) (rs.getObject("office_id") == null ? null : rs.getLong("office_id"))); - row.put("interest_rate", rs.getBigDecimal("interest_rate")); - row.put("deposit_amount", rs.getBigDecimal("deposit_amount")); - row.put("maturity_date", rs.getDate("maturity_date")); - row.put("closure_account_no", rs.getString("closure_account_no")); - row.put("closure_instruction", rs.getString("closure_instruction")); - return row; - }, params.toArray()); - - final List due = new ArrayList<>(); - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - for (final Map row : rows) { - final Long officeIdOf = (Long) row.get("office_id"); - if (officeIdOf == null) { - log.warn("FD maturity report: deposit {} has no branch and is left out", row.get("account_id")); - continue; - } - final BigDecimal estimated = previewInterest(((Number) row.get("account_id")).longValue(), - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - if (estimated == null) { - // Could not be priced; left out rather than approved against a guess. Missing from the - // report is visible, a wrong figure quietly approved is not. - continue; - } - due.add(new DueLine(((Number) row.get("account_id")).longValue(), (String) row.get("account_no"), - (String) row.get("client_name"), officeIdOf, ((Number) row.get("product_id")).longValue(), - (String) row.get("product_name"), (String) row.get("fd_type"), toLocalDate(row.get("maturity_date")), - (BigDecimal) row.get("deposit_amount"), (BigDecimal) row.get("interest_rate"), estimated, - (String) row.get("closure_instruction"), (String) row.get("closure_account_no"))); - } - return due; - } - - /** - * What {@code postMaturityInterest} would still credit this deposit, read without writing anything - * down. A separate, REQUIRES_NEW-free read: the account is loaded and discarded in this method's - * own Hibernate session with nothing marking it for a flush, since - * {@link FixedDepositAccount#previewMaturityInterestPeriods} only computes and returns periods. - * - * @return the total, zero for a deposit with nothing left to post, or null where it could not be - * priced - */ - private BigDecimal previewInterest(final long accountId, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, - final Integer financialYearBeginningMonth) { - try { - return this.readOnlyTransaction.execute(status -> { - final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(accountId, - DepositAccountType.FIXED_DEPOSIT); - final List unpaid = account.previewMaturityInterestPeriods(isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth); - BigDecimal total = BigDecimal.ZERO; - for (final PostingPeriod period : unpaid) { - total = total.add(period.getInterestEarned().getAmount()); - } - return total.setScale(MONEY_SCALE, MONEY_ROUNDING); - }); - } catch (final RuntimeException e) { - log.error("FD maturity report: deposit {} could not be priced and is left out of the report", accountId, e); - return null; - } - } - - // ── generation ─────────────────────────────────────────────────────────────────────────────── - - /** The noon job: generates a report for every branch with a deposit maturing and no report yet. */ - public List generateDue(final LocalDate postingDate) { - final List due = computeDue(postingDate, null); - final List generated = new ArrayList<>(); - final List failures = new ArrayList<>(); - for (final Map.Entry> group : groupByOffice(due).entrySet()) { - final Long officeId = group.getKey(); - if (activeReportId(officeId, postingDate) != null) { - continue; - } - try { - generated.add(this.transactionTemplate - .execute(status -> persistReport(officeId, postingDate, "AUTO", null, group.getValue()))); - } catch (final RuntimeException e) { - log.error("FD maturity report: branch {} could not be reported for {}", officeId, postingDate, e); - failures.add("branch " + officeId + ": " + e.getMessage()); - } - } - if (!failures.isEmpty()) { - throw new IllegalStateException( - "Fixed deposit maturity report could not be generated on " + postingDate + " for " + failures.size() - + " branch(es): " + String.join("; ", failures)); - } - return generated; - } - - /** Generates the report afresh for one branch, on the user's request. */ - public Long regenerate(final Long officeId, final LocalDate postingDate, final AppUser user) { - final LocalDate businessDate = DateUtils.getBusinessLocalDate(); - if (postingDate.isAfter(businessDate)) { - throw new PlatformDataIntegrityException("error.msg.fdmaturityreport.future.date", - "The report for " + postingDate + " cannot be generated before that date."); - } - authorize(user, officeId, businessDate); - final List due = computeDue(postingDate, officeId); - return this.transactionTemplate.execute(status -> persistReport(officeId, postingDate, "MANUAL", user.getId(), due)); - } - - public void approve(final Long reportId, final AppUser user) { - final List> found = this.jdbcTemplate - .queryForList("SELECT office_id, generated_by, status FROM m_fd_maturity_report WHERE id = ?", reportId); - if (found.isEmpty()) { - throw new PlatformDataIntegrityException("error.msg.fdmaturityreport.not.found", - "Fixed deposit maturity report " + reportId + " does not exist."); - } - final Map report = found.get(0); - final Long officeId = ((Number) report.get("office_id")).longValue(); - final Object generatedBy = report.get("generated_by"); - if (generatedBy != null && ((Number) generatedBy).longValue() == user.getId()) { - throw new PlatformDataIntegrityException("error.msg.fdmaturityreport.same.maker.checker", - "You generated this report, so you cannot approve it. Another user of this branch has to approve the maturities " - + "before they can be posted. Regenerating it yourself will not change that."); - } - final boolean onBehalf = authorize(user, officeId, DateUtils.getBusinessLocalDate()); - final int approved = this.jdbcTemplate.update(""" - UPDATE m_fd_maturity_report - SET status = 'APPROVED', approved_by = ?, approved_at = CURRENT_TIMESTAMP, approved_on_behalf = ? - WHERE id = ? AND status = 'GENERATED' - """, user.getId(), onBehalf, reportId); - if (approved == 0) { - throw new PlatformDataIntegrityException("error.msg.fdmaturityreport.not.generated", - "Only a report in GENERATED status can be approved. Reload the report — it may have been approved, " - + "regenerated or posted already."); - } - } - - private Long persistReport(final Long officeId, final LocalDate postingDate, final String mode, final Long userId, - final List lines) { - final List> previous = this.jdbcTemplate.queryForList(""" - SELECT id, status FROM m_fd_maturity_report - WHERE office_id = ? AND posting_date = ? AND status <> 'SUPERSEDED' - FOR UPDATE - """, officeId, postingDate); - for (final Map report : previous) { - if (POSTED.equals(report.get("status"))) { - throw new PlatformDataIntegrityException("error.msg.fdmaturityreport.already.posted", - "Fixed deposit maturities for this branch on " + postingDate + " have already been posted; the report cannot be" - + " regenerated."); - } - } - if (lines.isEmpty()) { - throw new PlatformDataIntegrityException("error.msg.fdmaturityreport.nothing.due", - "No fixed deposit is maturing in this branch on " + postingDate + "."); - } - this.jdbcTemplate.update(""" - UPDATE m_fd_maturity_report SET status = 'SUPERSEDED' - WHERE office_id = ? AND posting_date = ? AND status IN ('GENERATED', 'APPROVED') - """, officeId, postingDate); - - BigDecimal total = BigDecimal.ZERO; - for (final DueLine line : lines) { - total = total.add(line.estimatedInterest()); - } - - final Long reportId = this.jdbcTemplate.queryForObject(""" - INSERT INTO m_fd_maturity_report (office_id, posting_date, status, total_interest, account_count, generation_mode, - generated_by, generated_at) - VALUES (?, ?, 'GENERATED', ?, ?, ?, ?, CURRENT_TIMESTAMP) - RETURNING id - """, Long.class, officeId, postingDate, total, lines.size(), mode, userId); - - for (final DueLine line : lines) { - this.jdbcTemplate.update(""" - INSERT INTO m_fd_maturity_report_line - (report_id, fd_account_id, account_no, client_name, office_id, product_id, product_name, fd_type, - maturity_date, deposit_amount, interest_rate, estimated_interest, closure_instruction, closure_account_no) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, reportId, line.accountId(), line.accountNo(), line.clientName(), line.officeId(), line.productId(), - line.productName(), line.fdType(), line.maturityDate(), line.depositAmount(), line.interestRate(), - line.estimatedInterest(), line.closureInstruction(), line.closureAccountNo()); - } - - log.info("FD maturity report {}: branch {} on {} ({}) — {} across {} deposit(s)", reportId, officeId, postingDate, mode, total, - lines.size()); - return reportId; - } - - private Map> groupByOffice(final List lines) { - final Map> grouped = new LinkedHashMap<>(); - for (final DueLine line : lines) { - grouped.computeIfAbsent(line.officeId(), key -> new ArrayList<>()).add(line); - } - return grouped; - } - - // ── day close ──────────────────────────────────────────────────────────────────────────────── - - /** What stops this branch closing the day over maturities: due with no report, or an unapproved one. */ - public List reportsBlockingDayClose(final Long officeId, final LocalDate businessDate) { - if (!this.configurationDomainService.isFixedDepositMaturityInterestPostingApprovalEnabled()) { - return List.of(); - } - return blockingFrom(officeId, businessDate, computeDue(businessDate, officeId)); - } - - /** Both what is blocking the close over maturities, and the branch's maturing deposits and amounts. */ - public Map dayCloseMaturityView(final Long officeId, final LocalDate businessDate) { - final Map view = new LinkedHashMap<>(); - if (!this.configurationDomainService.isFixedDepositMaturityInterestPostingApprovalEnabled()) { - view.put("pending", List.of()); - view.put("due", List.of()); - return view; - } - final List due = computeDue(businessDate, officeId); - view.put("pending", blockingFrom(officeId, businessDate, due)); - - BigDecimal total = BigDecimal.ZERO; - for (final DueLine line : due) { - total = total.add(line.estimatedInterest()); - } - final Map row = new LinkedHashMap<>(); - row.put("accountCount", due.size()); - row.put("totalInterest", total.setScale(MONEY_SCALE, MONEY_ROUNDING)); - row.put("reportStatus", due.isEmpty() ? "NONE" : reportStatus(officeId, businessDate)); - view.put("due", due.isEmpty() ? List.of() : List.of(row)); - return view; - } - - private List blockingFrom(final Long officeId, final LocalDate businessDate, final List due) { - final Set blocking = new LinkedHashSet<>(); - if (!due.isEmpty() && activeApprovedOrPostedReportId(officeId, businessDate) == null) { - blocking.add("Fixed deposit maturities (" + businessDate + ")"); - } - for (final Map pending : this.jdbcTemplate.queryForList(""" - SELECT posting_date FROM m_fd_maturity_report - WHERE office_id = ? AND status = 'GENERATED' AND posting_date <= ? - ORDER BY posting_date - """, officeId, businessDate)) { - blocking.add("Fixed deposit maturities (" + pending.get("posting_date") + ")"); - } - return new ArrayList<>(blocking); - } - - private String reportStatus(final Long officeId, final LocalDate postingDate) { - final List status = this.jdbcTemplate.queryForList(""" - SELECT status FROM m_fd_maturity_report - WHERE office_id = ? AND posting_date = ? AND status <> 'SUPERSEDED' - ORDER BY id DESC LIMIT 1 - """, String.class, officeId, postingDate); - return status.isEmpty() ? "NONE" : status.get(0); - } - - /** The branch's approved reports, posted when it closes the day. */ - public List approvedReportIds(final Long officeId, final LocalDate upToDate) { - return this.jdbcTemplate.queryForList(""" - SELECT id FROM m_fd_maturity_report - WHERE status = 'APPROVED' AND office_id = ? AND posting_date <= ? - ORDER BY posting_date - """, Long.class, officeId, upToDate); - } - - public List approvedReportIdsOfBranchesNotOpened(final LocalDate businessDate, final Long headOfficeId) { - return this.jdbcTemplate.queryForList(""" - SELECT r.id FROM m_fd_maturity_report r - WHERE r.status = 'APPROVED' AND r.posting_date <= ? AND r.office_id <> ? - AND NOT EXISTS (SELECT 1 FROM m_branch_day_register b WHERE b.office_id = r.office_id - AND (b.business_date = ? OR (b.closed_at IS NULL AND b.business_date <= ?))) - ORDER BY r.posting_date, r.office_id - """, Long.class, businessDate, headOfficeId, businessDate, businessDate); - } - - public void recordPostingFailure(final Long reportId, final String reason) { - this.jdbcTemplate.update("UPDATE m_fd_maturity_report SET failure_reason = ? WHERE id = ?", - reason == null ? "unknown failure" : reason.length() > 1000 ? reason.substring(0, 1000) : reason, reportId); - } - - // ── reading ────────────────────────────────────────────────────────────────────────────────── - - public List> list(final LocalDate postingDate, final String status, final Long officeId, final String scope, - final AppUser user) { - final Long userOffice = officeOf(user); - final boolean others = "others".equalsIgnoreCase(scope) && this.branchAuthority.isHeadOffice(userOffice); - final Long branch = others ? officeId : userOffice; - - final StringBuilder sql = new StringBuilder(""" - SELECT r.id, r.office_id AS "officeId", o.name AS "officeName", r.posting_date AS "postingDate", r.status, - r.total_interest AS "totalInterest", r.account_count AS "accountCount", - r.generation_mode AS "generationMode", r.generated_at AS "generatedAt", gu.username AS "generatedBy", - r.generated_by AS "generatedById", r.approved_at AS "approvedAt", au.username AS "approvedBy", - r.approved_on_behalf AS "approvedOnBehalf", r.posted_at AS "postedAt", r.failure_reason AS "failureReason" - FROM m_fd_maturity_report r - LEFT JOIN m_office o ON o.id = r.office_id - LEFT JOIN m_appuser gu ON gu.id = r.generated_by - LEFT JOIN m_appuser au ON au.id = r.approved_by - WHERE 1 = 1 - """); - final List params = new ArrayList<>(); - if (postingDate != null) { - sql.append(" AND r.posting_date = ?"); - params.add(postingDate); - } - if (status != null && !status.isBlank()) { - sql.append(" AND r.status = ?"); - params.add(status.trim().toUpperCase()); - } else { - sql.append(" AND r.status <> 'SUPERSEDED'"); - } - if (branch != null) { - sql.append(" AND r.office_id = ?"); - params.add(branch); - } - if (others) { - sql.append(" AND r.office_id <> ?"); - params.add(userOffice); - } - sql.append(" ORDER BY r.posting_date DESC, o.name, r.id DESC"); - - final LocalDate businessDate = DateUtils.getBusinessLocalDate(); - final List> rows = new ArrayList<>(); - for (final Map row : this.jdbcTemplate.queryForList(sql.toString(), params.toArray())) { - final Map out = new LinkedHashMap<>(row); - final Object reportOffice = row.get("officeId"); - final String blocked = reportOffice == null ? "No branch on this report." - : actBlockedReason(userOffice, ((Number) reportOffice).longValue(), businessDate); - out.put("canAct", blocked == null); - out.put("blockedReason", blocked); - final Object generatedById = row.get("generatedById"); - final boolean ownWork = generatedById != null && user != null && ((Number) generatedById).longValue() == user.getId(); - out.put("canApprove", blocked == null && !ownWork && GENERATED.equals(row.get("status"))); - out.put("approveBlockedReason", ownWork ? "You generated this report — another user has to approve it." : blocked); - out.remove("generatedById"); - rows.add(out); - } - return rows; - } - - public Map detail(final Long reportId) { - final List> found = this.jdbcTemplate.queryForList(""" - SELECT r.id, r.office_id AS "officeId", o.name AS "officeName", r.posting_date AS "postingDate", r.status, - r.total_interest AS "totalInterest", r.account_count AS "accountCount", - r.generation_mode AS "generationMode", r.generated_at AS "generatedAt", gu.username AS "generatedBy", - r.approved_at AS "approvedAt", au.username AS "approvedBy", r.approved_on_behalf AS "approvedOnBehalf", - r.posted_at AS "postedAt", r.failure_reason AS "failureReason" - FROM m_fd_maturity_report r - LEFT JOIN m_office o ON o.id = r.office_id - LEFT JOIN m_appuser gu ON gu.id = r.generated_by - LEFT JOIN m_appuser au ON au.id = r.approved_by - WHERE r.id = ? - """, reportId); - if (found.isEmpty()) { - throw new PlatformDataIntegrityException("error.msg.fdmaturityreport.not.found", - "Fixed deposit maturity report " + reportId + " does not exist."); - } - final Map report = new LinkedHashMap<>(found.get(0)); - report.put("lines", this.jdbcTemplate.queryForList(""" - SELECT l.fd_account_id AS "fdAccountId", l.account_no AS "accountNo", l.client_name AS "clientName", - l.product_name AS "productName", l.fd_type AS "fdType", l.maturity_date AS "maturityDate", - l.deposit_amount AS "depositAmount", l.interest_rate AS "interestRate", - l.estimated_interest AS "estimatedInterest", l.closure_instruction AS "closureInstruction", - l.closure_account_no AS "closureAccountNo", l.posted_at AS "postedAt", l.failure_reason AS "failureReason" - FROM m_fd_maturity_report_line l - WHERE l.report_id = ? - ORDER BY l.account_no - """, reportId)); - return report; - } - - public List> dueWithoutReport(final LocalDate postingDate, final AppUser user, final String scope) { - final List> out = new ArrayList<>(); - final LocalDate businessDate = DateUtils.getBusinessLocalDate(); - if (postingDate.isAfter(businessDate)) { - return out; - } - final Long userOffice = officeOf(user); - final boolean others = "others".equalsIgnoreCase(scope) && this.branchAuthority.isHeadOffice(userOffice); - final List due = computeDue(postingDate, others ? null : userOffice); - for (final Map.Entry> group : groupByOffice(due).entrySet()) { - final Long officeId = group.getKey(); - if (others == officeId.equals(userOffice)) { - continue; - } - if (activeReportId(officeId, postingDate) != null) { - continue; - } - BigDecimal total = BigDecimal.ZERO; - for (final DueLine line : group.getValue()) { - total = total.add(line.estimatedInterest()); - } - final Map row = new LinkedHashMap<>(); - row.put("officeId", officeId); - row.put("officeName", officeName(officeId)); - row.put("postingDate", postingDate.toString()); - row.put("accountCount", group.getValue().size()); - row.put("totalInterest", total.setScale(MONEY_SCALE, MONEY_ROUNDING)); - final String blocked = actBlockedReason(userOffice, officeId, businessDate); - row.put("canAct", blocked == null); - row.put("blockedReason", blocked); - out.add(row); - } - return out; - } - - /** - * Fixed deposits that matured without this report ever seeing them — a deposit that named its own - * closure instruction (Transfer to Savings, either Reinvest option) settles directly, the moment it - * matures, exactly as it always has: the approval question is for a decision nobody has made yet, - * not one the customer already made in writing when the account was opened. Read-only, for - * visibility: a branch should be able to see every maturity that has happened, approved or not. - * - *

- * Found the same way the interest report's own reconciliation is: by what it is NOT, rather than by - * naming the closure instruction specifically — any matured deposit with no report line pointing at - * it, so a future direct path is caught here too rather than the list quietly going stale. - */ - public List> autoMatured(final LocalDate fromDate, final LocalDate toDate, final Long officeId, - final AppUser user, final String scope) { - final Long userOffice = officeOf(user); - final boolean others = "others".equalsIgnoreCase(scope) && this.branchAuthority.isHeadOffice(userOffice); - final Long branch = others ? officeId : userOffice; - - final StringBuilder sql = new StringBuilder(""" - SELECT sa.id AS "fdAccountId", sa.account_no AS "accountNo", - COALESCE(c.display_name, g.display_name) AS "clientName", - COALESCE(c.office_id, g.office_id) AS "officeId", o.name AS "officeName", - sa.product_id AS "productId", sp.name AS "productName", sp.fd_type AS "fdType", - datp.maturity_date AS "maturityDate", datp.deposit_amount AS "depositAmount", - COALESCE(sa.closedon_date, datp.maturity_date) AS "maturedOn", - """); - sql.append(CLOSURE_LABEL_CASE).append(" AS \"closureInstruction\"\n"); - sql.append(""" - FROM m_savings_account sa - JOIN m_savings_product sp ON sp.id = sa.product_id - JOIN m_deposit_account_term_and_preclosure datp ON datp.savings_account_id = sa.id - LEFT JOIN m_client c ON c.id = sa.client_id - LEFT JOIN m_group g ON g.id = sa.group_id - LEFT JOIN m_office o ON o.id = COALESCE(c.office_id, g.office_id) - WHERE sa.status_enum = 800 AND sa.deposit_type_enum = 200 - AND NOT EXISTS (SELECT 1 FROM m_fd_maturity_report_line l WHERE l.fd_account_id = sa.id) - """); - final List params = new ArrayList<>(); - if (fromDate != null) { - sql.append(" AND COALESCE(sa.closedon_date, datp.maturity_date) >= ?"); - params.add(fromDate); - } - if (toDate != null) { - sql.append(" AND COALESCE(sa.closedon_date, datp.maturity_date) <= ?"); - params.add(toDate); - } - if (branch != null) { - sql.append(" AND COALESCE(c.office_id, g.office_id) = ?"); - params.add(branch); - } else if (others) { - sql.append(" AND COALESCE(c.office_id, g.office_id) <> ?"); - params.add(userOffice); - } - sql.append(" ORDER BY sa.closedon_date DESC NULLS LAST, sa.account_no"); - return this.jdbcTemplate.queryForList(sql.toString(), params.toArray()); - } - - // ── who may act ────────────────────────────────────────────────────────────────────────────── - - private String actBlockedReason(final Long userOffice, final Long reportOffice, final LocalDate businessDate) { - if (userOffice == null) { - return "No branch is set for this user."; - } - if (userOffice.equals(reportOffice)) { - return null; - } - final String branch = officeName(reportOffice); - if (!this.branchAuthority.isHeadOffice(userOffice)) { - return "Only " + branch + " can generate or approve its own fixed deposit maturity report."; - } - if (this.branchAuthority.branchOpenedOn(reportOffice, businessDate)) { - return branch + " is open — it generates and approves its own report."; - } - final List stillOpen = this.branchAuthority.branchesStillOpen(businessDate, userOffice, reportOffice); - if (!stillOpen.isEmpty()) { - return "The head office can act for " + branch + " only after every open branch has closed. Still open: " - + String.join(", ", stillOpen) + "."; - } - return null; - } - - private boolean authorize(final AppUser user, final Long reportOffice, final LocalDate businessDate) { - final Long userOffice = officeOf(user); - final String blocked = actBlockedReason(userOffice, reportOffice, businessDate); - if (blocked != null) { - throw new PlatformDataIntegrityException("error.msg.fdmaturityreport.other.branch", blocked); - } - return !reportOffice.equals(userOffice); - } - - public boolean isHeadOffice(final Long officeId) { - return this.branchAuthority.isHeadOffice(officeId); - } - - // ── internals ──────────────────────────────────────────────────────────────────────────────── - - private Long activeReportId(final Long officeId, final LocalDate postingDate) { - final List ids = this.jdbcTemplate.queryForList(""" - SELECT id FROM m_fd_maturity_report WHERE office_id = ? AND posting_date = ? AND status <> 'SUPERSEDED' - """, Long.class, officeId, postingDate); - return ids.isEmpty() ? null : ids.get(0); - } - - private Long activeApprovedOrPostedReportId(final Long officeId, final LocalDate postingDate) { - final List ids = this.jdbcTemplate.queryForList(""" - SELECT id FROM m_fd_maturity_report WHERE office_id = ? AND posting_date = ? AND status IN ('APPROVED', 'POSTED') - """, Long.class, officeId, postingDate); - return ids.isEmpty() ? null : ids.get(0); - } - - private String officeName(final Long officeId) { - return this.jdbcTemplate.queryForObject("SELECT name FROM m_office WHERE id = ?", String.class, officeId); - } - - private static Long officeOf(final AppUser user) { - return user == null || user.getOffice() == null ? null : user.getOffice().getId(); - } - - private static LocalDate toLocalDate(final Object value) { - return value instanceof java.sql.Date date ? date.toLocalDate() : null; - } -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdmaturity/api/FixedDepositMaturityReportApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdmaturity/api/FixedDepositMaturityReportApiResource.java deleted file mode 100644 index bc1d2b635..000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdmaturity/api/FixedDepositMaturityReportApiResource.java +++ /dev/null @@ -1,198 +0,0 @@ -/** - * 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.fdmaturity.api; - -import com.google.gson.Gson; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import java.sql.Timestamp; -import java.time.LocalDate; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; -import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; -import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.portfolio.savings.fdmaturity.FixedDepositMaturityReportService; -import org.apache.fineract.useradministration.domain.AppUser; -import org.springframework.stereotype.Component; - -/** - * The fixed deposit maturity reports, per branch: list, detail, generate, approve, and the day-close - * check. Posting is not offered here — a branch's approved reports are posted when that branch closes - * the day, which is also when the real - * {@link org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService#updateMaturityDetails} - * actually runs for each approved deposit. - */ -@Path("/v1/fixeddepositmaturityreports") -@Component -@RequiredArgsConstructor -@Produces({ MediaType.APPLICATION_JSON }) -public class FixedDepositMaturityReportApiResource { - - private static final String RESOURCE = "FDMATURITYREPORT"; - - private final FixedDepositMaturityReportService reportService; - private final ConfigurationDomainService configurationDomainService; - private final PlatformSecurityContext context; - private final FromJsonHelper fromJsonHelper; - private final Gson gson = new Gson(); - - @GET - public String list(@QueryParam("postingDate") final String postingDate, @QueryParam("status") final String status, - @QueryParam("officeId") final Long officeId, @QueryParam("scope") final String scope) { - final AppUser user = this.context.authenticatedUser(); - user.validateHasReadPermission(RESOURCE); - return this.gson - .toJson(this.reportService.list(parseDate(postingDate), status, officeId, scope, user).stream().map(this::plain) - .toList()); - } - - @GET - @Path("/context") - public String userContext() { - final AppUser user = this.context.authenticatedUser(); - final Long officeId = user.getOffice().getId(); - final Map result = new LinkedHashMap<>(); - result.put("officeId", officeId); - result.put("officeName", user.getOffice().getName()); - result.put("headOffice", this.reportService.isHeadOffice(officeId)); - result.put("approvalEnabled", this.configurationDomainService.isFixedDepositMaturityInterestPostingApprovalEnabled()); - return this.gson.toJson(result); - } - - /** Branches with a deposit maturing by the date and no report yet. */ - @GET - @Path("/due-without-report") - public String dueWithoutReport(@QueryParam("postingDate") final String postingDate, @QueryParam("scope") final String scope) { - final AppUser user = this.context.authenticatedUser(); - user.validateHasReadPermission(RESOURCE); - final LocalDate date = postingDate == null || postingDate.isBlank() ? DateUtils.getBusinessLocalDate() : parseDate(postingDate); - return this.gson.toJson(this.reportService.dueWithoutReport(date, user, scope)); - } - - @GET - @Path("/day-close-check") - public String dayCloseCheck(@QueryParam("date") final String date, @QueryParam("officeId") final Long officeIdParam) { - final AppUser user = this.context.authenticatedUser(); - final LocalDate businessDate = date == null || date.isBlank() ? DateUtils.getBusinessLocalDate() : parseDate(date); - final Long officeId = officeIdParam != null ? officeIdParam : user.getOffice().getId(); - final Map view = this.reportService.dayCloseMaturityView(officeId, businessDate); - @SuppressWarnings("unchecked") - final List blocking = (List) view.get("pending"); - final Map result = new LinkedHashMap<>(); - result.put("businessDate", businessDate.toString()); - result.put("canClose", blocking.isEmpty()); - result.put("pendingProducts", blocking); - result.put("due", view.get("due")); - return this.gson.toJson(result); - } - - /** - * Fixed deposits that matured without this report ever seeing them — chiefly a deposit with its own - * closure instruction, which still settles directly the moment it matures. - */ - @GET - @Path("/outside-approval") - public String autoMatured(@QueryParam("fromDate") final String fromDate, @QueryParam("toDate") final String toDate, - @QueryParam("officeId") final Long officeId, @QueryParam("scope") final String scope) { - final AppUser user = this.context.authenticatedUser(); - user.validateHasReadPermission(RESOURCE); - return this.gson.toJson( - this.reportService.autoMatured(parseDate(fromDate), parseDate(toDate), officeId, user, scope).stream().map(this::plain) - .toList()); - } - - @GET - @Path("/{reportId}") - public String detail(@PathParam("reportId") final Long reportId) { - this.context.authenticatedUser().validateHasReadPermission(RESOURCE); - final Map report = plain(this.reportService.detail(reportId)); - @SuppressWarnings("unchecked") - final List> lines = (List>) report.get("lines"); - report.put("lines", lines.stream().map(this::plain).toList()); - return this.gson.toJson(report); - } - - /** {@code ?command=regenerate} with {@code {"officeId": .., "postingDate": "yyyy-MM-dd"}}. */ - @POST - @Consumes({ MediaType.APPLICATION_JSON }) - public String command(@QueryParam("command") final String command, final String body) { - if (!"regenerate".equalsIgnoreCase(command)) { - throw new PlatformDataIntegrityException("error.msg.fdmaturityreport.unknown.command", "Unsupported command: " + command); - } - final AppUser user = this.context.authenticatedUser(); - user.validateHasPermissionTo("REGENERATE_" + RESOURCE); - final var root = this.fromJsonHelper.parse(body); - final Long officeIdParam = this.fromJsonHelper.extractLongNamed("officeId", root); - final String postingDate = this.fromJsonHelper.extractStringNamed("postingDate", root); - final Long officeId = officeIdParam != null ? officeIdParam : user.getOffice().getId(); - final LocalDate date = postingDate == null || postingDate.isBlank() ? DateUtils.getBusinessLocalDate() : parseDate(postingDate); - final Long reportId = this.reportService.regenerate(officeId, date, user); - return this.gson.toJson(Map.of("reportId", reportId, "status", FixedDepositMaturityReportService.GENERATED)); - } - - @POST - @Path("/{reportId}") - @Consumes({ MediaType.APPLICATION_JSON }) - public String reportCommand(@PathParam("reportId") final Long reportId, @QueryParam("command") final String command) { - if (!"approve".equalsIgnoreCase(command)) { - throw new PlatformDataIntegrityException("error.msg.fdmaturityreport.unknown.command", "Unsupported command: " + command); - } - final AppUser user = this.context.authenticatedUser(); - user.validateHasPermissionTo("APPROVE_" + RESOURCE); - this.reportService.approve(reportId, user); - return this.gson.toJson(Map.of("reportId", reportId, "status", FixedDepositMaturityReportService.APPROVED)); - } - - private static LocalDate parseDate(final String raw) { - if (raw == null || raw.isBlank()) { - return null; - } - try { - return LocalDate.parse(raw.trim()); - } catch (final RuntimeException e) { - throw new PlatformDataIntegrityException("error.msg.fdmaturityreport.invalid.date", "Dates are expected as yyyy-MM-dd, not " - + raw); - } - } - - private Map plain(final Map row) { - final Map out = new LinkedHashMap<>(); - row.forEach((key, value) -> { - if (value instanceof java.sql.Date d) { - out.put(key, d.toLocalDate().toString()); - } else if (value instanceof Timestamp t) { - out.put(key, t.toLocalDateTime().toString()); - } else { - out.put(key, value); - } - }); - return out; - } -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdmaturity/jobs/FixedDepositMaturityReportConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdmaturity/jobs/FixedDepositMaturityReportConfig.java deleted file mode 100644 index d56cb00e9..000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdmaturity/jobs/FixedDepositMaturityReportConfig.java +++ /dev/null @@ -1,52 +0,0 @@ -/** - * 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.fdmaturity.jobs; - -import org.apache.fineract.infrastructure.jobs.service.JobName; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.Step; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.launch.support.RunIdIncrementer; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.transaction.PlatformTransactionManager; - -@Configuration -public class FixedDepositMaturityReportConfig { - - @Autowired - private JobRepository jobRepository; - @Autowired - private PlatformTransactionManager transactionManager; - - @Bean - protected Step fdMaturityReportStep(final FixedDepositMaturityReportTasklet fdMaturityReportTasklet) { - return new StepBuilder(JobName.FD_MATURITY_REPORT.name(), jobRepository).tasklet(fdMaturityReportTasklet, transactionManager) - .build(); - } - - @Bean - public Job fdMaturityReportJob(final FixedDepositMaturityReportTasklet fdMaturityReportTasklet) { - return new JobBuilder(JobName.FD_MATURITY_REPORT.name(), jobRepository).start(fdMaturityReportStep(fdMaturityReportTasklet)) - .incrementer(new RunIdIncrementer()).build(); - } -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdmaturity/jobs/FixedDepositMaturityReportTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdmaturity/jobs/FixedDepositMaturityReportTasklet.java deleted file mode 100644 index f4b498a4e..000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/fdmaturity/jobs/FixedDepositMaturityReportTasklet.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * 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.fdmaturity.jobs; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.portfolio.savings.fdmaturity.FixedDepositMaturityReportService; -import org.springframework.batch.core.StepContribution; -import org.springframework.batch.core.scope.context.ChunkContext; -import org.springframework.batch.core.step.tasklet.Tasklet; -import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.stereotype.Component; - -/** - * Noon on the business date: generates the maturity report for every branch with a fixed deposit - * maturing, so it can be reviewed and approved before the branch closes the day. - * - *

- * Does nothing while the gate is off — the nightly maturity job settles matured deposits directly, and - * a report generated alongside it would be approved against an event that has already happened. - */ -@RequiredArgsConstructor -@Component -@Slf4j -public class FixedDepositMaturityReportTasklet implements Tasklet { - - private final FixedDepositMaturityReportService reportService; - private final ConfigurationDomainService configurationDomainService; - - @Override - public RepeatStatus execute(final StepContribution contribution, final ChunkContext chunkContext) throws Exception { - if (!this.configurationDomainService.isFixedDepositMaturityInterestPostingApprovalEnabled()) { - log.info("FD maturity report: skipped — fixed deposit maturities are settled directly by the nightly job"); - return RepeatStatus.FINISHED; - } - this.reportService.generateDue(DateUtils.getBusinessLocalDate()); - return RepeatStatus.FINISHED; - } -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/updatedepositsaccountmaturitydetails/UpdateDepositsAccountMaturityDetailsConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/updatedepositsaccountmaturitydetails/UpdateDepositsAccountMaturityDetailsConfig.java index 83cb843de..0a02bfc77 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/updatedepositsaccountmaturitydetails/UpdateDepositsAccountMaturityDetailsConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/updatedepositsaccountmaturitydetails/UpdateDepositsAccountMaturityDetailsConfig.java @@ -18,7 +18,6 @@ */ package org.apache.fineract.portfolio.savings.jobs.updatedepositsaccountmaturitydetails; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.jobs.service.JobName; import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService; import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService; @@ -31,7 +30,6 @@ import org.springframework.batch.core.step.builder.StepBuilder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.PlatformTransactionManager; @Configuration @@ -45,10 +43,6 @@ public class UpdateDepositsAccountMaturityDetailsConfig { private DepositAccountReadPlatformService depositAccountReadPlatformService; @Autowired private DepositAccountWritePlatformService depositAccountWritePlatformService; - @Autowired - private ConfigurationDomainService configurationDomainService; - @Autowired - private JdbcTemplate jdbcTemplate; @Bean protected Step updateDepositsAccountMaturityDetailsStep() { @@ -64,7 +58,6 @@ public class UpdateDepositsAccountMaturityDetailsConfig { @Bean public UpdateDepositsAccountMaturityDetailsTasklet updateDepositsAccountMaturityDetailsTasklet() { - return new UpdateDepositsAccountMaturityDetailsTasklet(depositAccountReadPlatformService, depositAccountWritePlatformService, - configurationDomainService, jdbcTemplate); + return new UpdateDepositsAccountMaturityDetailsTasklet(depositAccountReadPlatformService, depositAccountWritePlatformService); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/updatedepositsaccountmaturitydetails/UpdateDepositsAccountMaturityDetailsTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/updatedepositsaccountmaturitydetails/UpdateDepositsAccountMaturityDetailsTasklet.java index 7e8a6760c..5f956586a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/updatedepositsaccountmaturitydetails/UpdateDepositsAccountMaturityDetailsTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/updatedepositsaccountmaturitydetails/UpdateDepositsAccountMaturityDetailsTasklet.java @@ -22,7 +22,6 @@ import java.util.Collection; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; @@ -34,7 +33,6 @@ import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.jdbc.core.JdbcTemplate; @Slf4j @RequiredArgsConstructor @@ -42,27 +40,15 @@ public class UpdateDepositsAccountMaturityDetailsTasklet implements Tasklet { private final DepositAccountReadPlatformService depositAccountReadPlatformService; private final DepositAccountWritePlatformService depositAccountWritePlatformService; - private final ConfigurationDomainService configurationDomainService; - private final JdbcTemplate jdbcTemplate; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { final Collection depositAccounts = depositAccountReadPlatformService.retrieveForMaturityUpdate(); - // With the gate on, a fixed deposit with NO maturity instruction waits for - // FixedDepositMaturityReportService's report to be approved before it settles, rather than this - // job running it directly the moment it matures. One that carries its own instruction (Transfer - // to Savings, either Reinvest option) still settles here, exactly as it always has — the - // approval question does not apply to a decision the customer already made in writing, only to - // the one nobody has made yet. Recurring deposits are unaffected either way. - final boolean fdMaturityGated = this.configurationDomainService.isFixedDepositMaturityInterestPostingApprovalEnabled(); for (final DepositAccountData depositAccount : depositAccounts) { try { final DepositAccountType depositAccountType = DepositAccountType .fromInt(depositAccount.getDepositType().getId().intValue()); - if (fdMaturityGated && depositAccountType.isFixedDeposit() && hasNoClosureInstruction(depositAccount.getId())) { - continue; - } depositAccountWritePlatformService.updateMaturityDetails(depositAccount.getId(), depositAccountType); } catch (final PlatformApiDataValidationException e) { final List errors = e.getErrors(); @@ -79,12 +65,4 @@ public class UpdateDepositsAccountMaturityDetailsTasklet implements Tasklet { depositAccounts.size()); return RepeatStatus.FINISHED; } - - /** True where the account names no closure instruction, including one with no term row at all. */ - private boolean hasNoClosureInstruction(final Long accountId) { - final List found = this.jdbcTemplate.queryForList( - "SELECT on_account_closure_enum FROM m_deposit_account_term_and_preclosure WHERE savings_account_id = ?", Integer.class, - accountId); - return found.isEmpty() || found.get(0) == null || found.get(0) == 0; - } } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml index b8115ea1b..470c64207 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml @@ -166,5 +166,4 @@ - diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0594_fd_maturity_interest_posting_approval.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0594_fd_maturity_interest_posting_approval.xml deleted file mode 100644 index d7e28ad9b..000000000 --- a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0594_fd_maturity_interest_posting_approval.xml +++ /dev/null @@ -1,298 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SELECT COUNT(*) FROM job WHERE name = 'Generate Fixed Deposit Maturity Report' - - - - - - - - - - - - - - - - - - - - - - - - - - SELECT COUNT(*) FROM m_permission WHERE code IN ('READ_FDMATURITYREPORT', - 'REGENERATE_FDMATURITYREPORT', 'APPROVE_FDMATURITYREPORT') - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SELECT COUNT(*) FROM m_role_permission rp - JOIN m_permission p ON p.id = rp.permission_id - WHERE p.code IN ('READ_FDMATURITYREPORT', 'REGENERATE_FDMATURITYREPORT', - 'APPROVE_FDMATURITYREPORT') - - - - INSERT INTO m_role_permission (role_id, permission_id) - SELECT r.id, p.id - FROM m_role r - CROSS JOIN m_permission p - WHERE p.code IN ('READ_FDMATURITYREPORT', 'REGENERATE_FDMATURITYREPORT', - 'APPROVE_FDMATURITYREPORT') - AND (LOWER(r.name) LIKE '%manager%' OR LOWER(r.name) = 'super user') - AND NOT EXISTS ( - SELECT 1 FROM m_role_permission rp - WHERE rp.role_id = r.id AND rp.permission_id = p.id - ) - - - - - - - - SELECT COUNT(*) FROM c_configuration WHERE name = 'fd-maturity-interest-posting-approval' - - - - - - - - - - - -