From 87f3e591ce6aee9c51605dde39a6339125b3d89e Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Mon, 28 Sep 2026 10:34:39 +0530 Subject: [PATCH] fix(accounting): tag current-account GL entries entity_type_enum=CURRENT instead of reusing SAVING, everywhere this codebase links an entry to its account m_current_account and m_savings_account each keep their own identity sequence, so the same numeric id can be a real row in both. Every place that links a GL journal entry back to its account for display -- so the Journal Entries screen shows an Account No instead of a blank "-" -- was tagging entity_type_enum=2 (PortfolioProductType.SAVING) unconditionally, even when the account was actually a current account. Whenever a current account's id collided with an unrelated savings account's id, the entry resolved to the wrong one entirely: a customer's savings deposit could display someone else's current account number, or vice versa. Fixed at the write side, in every place found doing this: - ClerkDisbursementRequestApiResource (Bank Transfer, Collection, Disbursement) -- GlEntry now carries an isCurrentAccount flag, decided once where the account was already resolved, instead of being re-guessed. - ChequeBookIssuanceChargeService, ClearingJournalService, SponsorDDIssueRequestWritePlatformServiceImpl -- same pattern, each already knew savings vs current at the point it linked the entry; that answer is now threaded through instead of discarded. Also fixed on the read side: - JournalEntryReadPlatformServiceImpl: entity_type_enum=6 now resolves unambiguously to the current account; entity_type_enum=2 still falls back to savings_transaction_id (a reliable signal only a genuine savings transaction sets) before guessing, for older rows written before this fix. - JournalEntryReferralServiceImpl (AML referral resolution): the same ambiguity could attribute a suspicious-activity alert to the wrong client entirely -- a same-numbered stranger's account -- when a current account's entity_id collided with a savings account's. Resolved with the same entity_type=6 / savings_transaction_id precedence as the read-side fix. Not touched: JournalEntryAggregationJobReader (year-end aggregation batch job) has the same ambiguity for legacy entity_type_enum=2 rows, but only ever joins m_savings_account -- it has no current-account handling at all, standard-Fineract in origin. Extending it is a real feature addition, not a targeted fix, and touches a scheduled financial batch job this session cannot rebuild or run to verify; flagged for separate, deliberate work instead. Co-Authored-By: Claude Sonnet 5 --- .../JournalEntryReadPlatformServiceImpl.java | 27 ++++++----- .../JournalEntryReferralServiceImpl.java | 16 +++++-- .../ChequeBookIssuanceChargeService.java | 28 ++++++++--- .../service/ClearingJournalService.java | 18 ++++++-- .../ClerkDisbursementRequestApiResource.java | 46 +++++++++++++------ ...DIssueRequestWritePlatformServiceImpl.java | 35 ++++++++++---- 6 files changed, 123 insertions(+), 47 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java index 8115f66ea..7169ba0ab 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java @@ -105,17 +105,22 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat .append(" journalEntry.currency_code as currencyCode, curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ") .append(" curr.display_symbol as currencyDisplaySymbol, curr.decimal_places as currencyDigits, curr.currency_multiplesof as inMultiplesOf, ") .append(" eao.external_id as externalAssetOwner, ") - // entity_type_enum=2 (PortfolioProductType.SAVING) is written for BOTH savings and - // current-account entries -- current products deliberately share SAVING's GL type - // space (see PortfolioProductType.CURRENT's own comment) -- so it cannot tell the two - // apart on its own, and m_savings_account/m_current_account each have their own id - // sequence: an entity_id can be a real row in both, making a plain - // COALESCE(ca.account_no, sa.account_no) pick the current account's number even for - // an entry that is genuinely a savings one. savings_transaction_id is the one - // unambiguous signal available here -- only a real savings transaction sets it -- so - // it decides first; only when it's absent (a current-account entry, or a savings - // entry with no linked transaction row) does the current-account guess apply. + // entity_type_enum=6 (PortfolioProductType.CURRENT) unambiguously names a row in + // m_current_account -- newer write paths tag current-account entries with it + // instead of reusing SAVING's type, specifically so this case needs no guessing. + // + // entity_type_enum=2 (PortfolioProductType.SAVING) is still written for BOTH savings + // and current-account entries by older write paths not yet updated to the above -- + // current products used to deliberately share SAVING's GL type space (see + // PortfolioProductType.CURRENT's own comment) -- and m_savings_account/ + // m_current_account each have their own id sequence, so an entity_id can be a real + // row in both, making a plain COALESCE(ca.account_no, sa.account_no) pick the current + // account's number even for an entry that is genuinely a savings one. + // savings_transaction_id is the one unambiguous signal available for those older + // entries -- only a real savings transaction sets it -- so it decides first; only + // when it's absent does the current-account guess apply. .append(" CASE ") + .append(" WHEN journalEntry.entity_type_enum = 6 THEN ca.account_no ") .append(" WHEN journalEntry.entity_type_enum = 2 AND journalEntry.savings_transaction_id IS NOT NULL THEN sa.account_no ") .append(" WHEN journalEntry.entity_type_enum = 2 THEN COALESCE(ca.account_no, sa.account_no) ") .append(" ELSE NULL ") @@ -141,7 +146,7 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat .append(" left join m_external_asset_owner_journal_entry_mapping eajem on eajem.journal_entry_id = journalEntry.id ") .append(" left join m_external_asset_owner eao on eao.id = eajem.owner_id ") .append(" left join m_savings_account sa on sa.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 ") - .append(" left join m_current_account ca on ca.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 "); + .append(" left join m_current_account ca on ca.id = journalEntry.entity_id AND journalEntry.entity_type_enum IN (2, 6) "); if (associationParametersData.isTransactionDetailsRequired()) { sb.append(" left join m_loan_transaction as lt on journalEntry.loan_transaction_id = lt.id ") .append(" left join m_savings_account_transaction as st on journalEntry.savings_transaction_id = st.id ") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/aml/service/JournalEntryReferralServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/aml/service/JournalEntryReferralServiceImpl.java index db4ec204d..0e1e65ff3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/aml/service/JournalEntryReferralServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/aml/service/JournalEntryReferralServiceImpl.java @@ -68,6 +68,15 @@ public class JournalEntryReferralServiceImpl implements JournalEntryReferralServ * The transaction-id columns are kept as a fallback for entries that carry one but no entity. A purely manual GL * posting reaches none of these and comes back with a null clientId, which is what makes it unreferrable. */ + // m_savings_account and m_current_account each keep their own identity sequence, so the same + // entity_id can be a real row in both -- entity_type_enum=2 (PortfolioProductType.SAVING) used to + // be written for both savings and current-account entries alike, so esa/eca could both match the + // same id and a plain COALESCE(esa.client_id, eca.client_id) would attribute an alert to whichever + // client happened to hold the same-numbered account on the OTHER ledger -- exactly the kind of + // misattribution an AML referral cannot afford. entity_type_enum=6 (PortfolioProductType.CURRENT) + // is what newer write paths tag current-account entries with instead, unambiguously; for older + // entries still tagged 2, savings_transaction_id is the one reliable signal (only a genuine + // savings transaction sets it), checked before the ambiguous current/savings guess. private static final String RESOLVE_SQL = """ SELECT je.amount AS amount, je.currency_code AS currencyCode, @@ -75,8 +84,9 @@ public class JournalEntryReferralServiceImpl implements JournalEntryReferralServ je.loan_transaction_id AS loanTxnId, je.entity_type_enum AS entityType, COALESCE( - esa.client_id, - eca.client_id, + CASE WHEN je.entity_type_enum = 2 AND je.savings_transaction_id IS NOT NULL THEN sa.client_id END, + CASE WHEN je.entity_type_enum = 6 THEN eca.client_id END, + CASE WHEN je.entity_type_enum = 2 THEN COALESCE(eca.client_id, esa.client_id) END, eloan.client_id, CASE WHEN je.entity_type_enum = 5 THEN je.entity_id END, sa.client_id, @@ -85,7 +95,7 @@ public class JournalEntryReferralServiceImpl implements JournalEntryReferralServ ) AS clientId FROM acc_gl_journal_entry je LEFT JOIN m_savings_account esa ON esa.id = je.entity_id AND je.entity_type_enum = 2 - LEFT JOIN m_current_account eca ON eca.id = je.entity_id AND je.entity_type_enum = 2 + LEFT JOIN m_current_account eca ON eca.id = je.entity_id AND je.entity_type_enum IN (2, 6) LEFT JOIN m_loan eloan ON eloan.id = je.entity_id AND je.entity_type_enum = 1 LEFT JOIN m_savings_account_transaction sat ON sat.id = je.savings_transaction_id LEFT JOIN m_savings_account sa ON sa.id = sat.savings_account_id diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookIssuanceChargeService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookIssuanceChargeService.java index 66d0c6950..1954371b3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookIssuanceChargeService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookIssuanceChargeService.java @@ -40,6 +40,7 @@ import org.apache.fineract.portfolio.voucher.service.VoucherLine; import org.apache.fineract.portfolio.voucher.service.VoucherService; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.chequebook.domain.ChequeAccountType; import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount; @@ -252,7 +253,7 @@ public class ChequeBookIssuanceChargeService { } postChargeJournalEntry(office.getId(), charge.getCurrencyCode(), today, ownAccountGlId, incomeGlId, chargeAmount, chequeBookId, - accountId); + accountId, accountType); raiseChargeVoucher(accountType, accountId, ownAccountGlId, incomeGlId, chargeAmount, chequeBookId); log.info("Deducted cheque book issuance charge {} from {} account {} for book {}", chargeAmount, accountType, accountId, chequeBookId); @@ -346,7 +347,8 @@ public class ChequeBookIssuanceChargeService { /** DR the account's own control GL (liability down), CR the charge's income GL (fee income). */ private void postChargeJournalEntry(final Long officeId, final String currencyCode, final LocalDate txnDate, final Long debitGlId, - final Long creditGlId, final BigDecimal amount, final Long chequeBookId, final Long accountId) { + final Long creditGlId, final BigDecimal amount, final Long chequeBookId, final Long accountId, + final ChequeAccountType accountType) { final JsonArray debitArr = new JsonArray(); final JsonObject debit = new JsonObject(); debit.addProperty("glAccountId", debitGlId); @@ -383,12 +385,21 @@ public class ChequeBookIssuanceChargeService { // link it back to the customer's account, same as ClerkDisbursementRequestApiResource's direct debits do. final String glTxnId = result.getTransactionId(); if (glTxnId != null) { - linkDebitEntryToAccount(glTxnId, accountId); + linkDebitEntryToAccount(glTxnId, accountId, accountType); } } - /** Links the DEBIT row (type_enum=2) of this journal transaction to the customer's account so it shows an Account No. */ - private void linkDebitEntryToAccount(final String glTxnId, final Long accountId) { + /** + * Links the DEBIT row (type_enum=2) of this journal transaction to the customer's account so it shows an + * Account No. + * + * m_current_account and m_savings_account each keep their own identity sequence, so the same numeric id can + * be a real row in both -- tagging every entry entity_type_enum=2 (PortfolioProductType.SAVING) regardless of + * accountType made the Journal Entries screen resolve a current account's charge to a same-numbered savings + * account instead (or vice versa). entity_type_enum=6 (PortfolioProductType.CURRENT) is the one that actually + * means "this id is in m_current_account" -- use it whenever accountType says so. + */ + private void linkDebitEntryToAccount(final String glTxnId, final Long accountId, final ChequeAccountType accountType) { // ORDER BY id DESC: prefer the most recently posted row for this transaction_id text, in // case the id is ever reused (e.g. after a sequence reset) rather than latching onto a // stale, unrelated journal entry. @@ -397,8 +408,11 @@ public class ChequeBookIssuanceChargeService { + " ORDER BY id DESC LIMIT 1", Long.class, glTxnId); if (!rowIds.isEmpty()) { - this.jdbcTemplate.update("UPDATE acc_gl_journal_entry SET entity_type_enum = 2, entity_id = ? WHERE id = ?", accountId, - rowIds.get(0)); + final int entityType = accountType == ChequeAccountType.CURRENT + ? PortfolioProductType.CURRENT.getValue() + : PortfolioProductType.SAVING.getValue(); + this.jdbcTemplate.update("UPDATE acc_gl_journal_entry SET entity_type_enum = ?, entity_id = ? WHERE id = ?", entityType, + accountId, rowIds.get(0)); } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clearing/service/ClearingJournalService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clearing/service/ClearingJournalService.java index 9180b5bf6..535ce45c9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clearing/service/ClearingJournalService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clearing/service/ClearingJournalService.java @@ -34,6 +34,7 @@ import org.apache.fineract.commands.service.SynchronousCommandProcessingService; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.voucher.service.VoucherLine; import org.apache.fineract.portfolio.voucher.service.VoucherService; import org.springframework.jdbc.core.JdbcTemplate; @@ -118,7 +119,7 @@ public class ClearingJournalService { } final String glTxnId = result.getTransactionId(); if (glTxnId != null && linkAccountId != null) { - linkDebitEntryToAccount(glTxnId, linkAccountId); + linkDebitEntryToAccount(glTxnId, linkAccountId, linkAccountType); } raiseClearingVoucher(glTxnId, debitGlId, creditGlId, amount, comments, linkAccountId, linkAccountType); return glTxnId; @@ -191,7 +192,13 @@ public class ClearingJournalService { return this.jdbcTemplate.queryForList(sql, accountId).stream().findFirst().orElse(Map.of()); } - private void linkDebitEntryToAccount(final String glTxnId, final Long accountId) { + /** + * Same savings-vs-current ambiguity {@link #accountHolderForVoucher} already guards against: the two + * account tables number independently, so entity_type_enum must say which one accountId names, not + * assume SAVING(2) for both. entity_type_enum=6 (PortfolioProductType.CURRENT) is what actually means + * "this id is in m_current_account". + */ + private void linkDebitEntryToAccount(final String glTxnId, final Long accountId, final String accountType) { // ORDER BY id DESC: prefer the most recently posted row for this transaction_id text, in // case the id is ever reused (e.g. after a sequence reset) rather than latching onto a // stale, unrelated journal entry. @@ -200,8 +207,11 @@ public class ClearingJournalService { + " ORDER BY id DESC LIMIT 1", Long.class, glTxnId); if (!rowIds.isEmpty()) { - this.jdbcTemplate.update("UPDATE acc_gl_journal_entry SET entity_type_enum = 2, entity_id = ? WHERE id = ?", accountId, - rowIds.get(0)); + final int entityType = "CURRENT".equalsIgnoreCase(accountType) + ? PortfolioProductType.CURRENT.getValue() + : PortfolioProductType.SAVING.getValue(); + this.jdbcTemplate.update("UPDATE acc_gl_journal_entry SET entity_type_enum = ?, entity_id = ? WHERE id = ?", entityType, + accountId, rowIds.get(0)); } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkdisbursement/api/ClerkDisbursementRequestApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkdisbursement/api/ClerkDisbursementRequestApiResource.java index b94837355..56f55e3ba 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkdisbursement/api/ClerkDisbursementRequestApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkdisbursement/api/ClerkDisbursementRequestApiResource.java @@ -66,6 +66,7 @@ import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHold import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessSucceedException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.voucher.service.VoucherLine; import org.apache.fineract.portfolio.client.service.KycRestrictionGuard; import org.apache.fineract.portfolio.voucher.service.VoucherService; @@ -568,7 +569,7 @@ public class ClerkDisbursementRequestApiResource { ? directDebitCurrentAccount(accountId, entryAmt, officeId, txnDateStr, currentUser, description) : directDebitSavingsAccount(accountId, entryAmt, officeId, txnDateStr, currentUser, description); txnRefs.add(String.valueOf(txnId)); - btDebitGlEntries.add(new GlEntry(ctrlGl, entryAmt, acNo, null, accountId)); + btDebitGlEntries.add(new GlEntry(ctrlGl, entryAmt, acNo, null, accountId, isCurrent)); btTotalDr = btTotalDr.add(entryAmt); if (isCurrent) undoOps.add(() -> directReverseCurrentDebit(accountId, entryAmt, txnId)); else undoOps.add(() -> directReverseSavingsDebit(accountId, entryAmt, txnId)); @@ -577,7 +578,7 @@ public class ClerkDisbursementRequestApiResource { ? directCreditCurrentAccount(accountId, entryAmt, officeId, txnDateStr, currentUser, description) : directCreditSavingsAccount(accountId, entryAmt, officeId, txnDateStr, currentUser); txnRefs.add(String.valueOf(txnId)); - btCreditGlEntries.add(new GlEntry(ctrlGl, entryAmt, acNo, null, accountId)); + btCreditGlEntries.add(new GlEntry(ctrlGl, entryAmt, acNo, null, accountId, isCurrent)); if (isCurrent) undoOps.add(() -> directReverseCurrentCredit(accountId, entryAmt, txnId)); else undoOps.add(() -> directReverseSavingsCredit(accountId, entryAmt, txnId)); } @@ -681,7 +682,7 @@ public class ClerkDisbursementRequestApiResource { accountId, entryAmt, officeId, txnDateStr, currentUser, description); txnRefs.add(String.valueOf(directCaTxnId)); depositSavingsTotal = depositSavingsTotal.add(entryAmt); - disbursementGlEntries.add(new GlEntry(caCtrlGl, entryAmt, acNo, null, accountId)); + disbursementGlEntries.add(new GlEntry(caCtrlGl, entryAmt, acNo, null, accountId, true)); undoOps.add(() -> directReverseCurrentCredit(accountId, entryAmt, directCaTxnId)); } else { // Real CBS book transfer: direct credit to savings account. @@ -696,7 +697,7 @@ public class ClerkDisbursementRequestApiResource { accountId, entryAmt, officeId, txnDateStr, currentUser); txnRefs.add(String.valueOf(directTxnId)); depositSavingsTotal = depositSavingsTotal.add(entryAmt); - disbursementGlEntries.add(new GlEntry(savingsCtrlGl, entryAmt, acNo, null, accountId)); + disbursementGlEntries.add(new GlEntry(savingsCtrlGl, entryAmt, acNo, null, accountId, false)); undoOps.add(() -> directReverseSavingsCredit(accountId, entryAmt, directTxnId)); } } else { @@ -715,7 +716,7 @@ public class ClerkDisbursementRequestApiResource { : directDebitSavingsAccount(accountId, entryAmt, officeId, txnDateStr, currentUser, description); txnRefs.add(String.valueOf(debitTxnId)); withdrawalSavingsTotal = withdrawalSavingsTotal.add(entryAmt); - collectionGlEntries.add(new GlEntry(ctrlGl, entryAmt, acNo, null, accountId)); + collectionGlEntries.add(new GlEntry(ctrlGl, entryAmt, acNo, null, accountId, isCurrentAccount)); if (isCurrentAccount) { undoOps.add(() -> directReverseCurrentDebit(accountId, entryAmt, debitTxnId)); } else { @@ -979,7 +980,7 @@ public class ClerkDisbursementRequestApiResource { : directDebitSavingsAccount(accountId, amount, officeId, txnDateStr, currentUser, description); txnRefs.add(String.valueOf(txnId)); - debitGlEntries.add(new GlEntry(ctrlGl, amount, acNo, null, accountId)); + debitGlEntries.add(new GlEntry(ctrlGl, amount, acNo, null, accountId, isCurrent)); if (isCurrent) { undoOps.add(() -> directReverseCurrentDebit(accountId, amount, txnId)); } else { @@ -1005,7 +1006,7 @@ public class ClerkDisbursementRequestApiResource { ? directCreditCurrentAccount(accountId, amount, officeId, txnDateStr, currentUser, description) : directCreditSavingsAccount(accountId, amount, officeId, txnDateStr, currentUser); txnRefs.add(String.valueOf(txnId)); - creditGlEntries.add(new GlEntry(ctrlGl, amount, acNo, null, accountId)); + creditGlEntries.add(new GlEntry(ctrlGl, amount, acNo, null, accountId, isCurrent)); if (isCurrent) { undoOps.add(() -> directReverseCurrentCredit(accountId, amount, txnId)); } else { @@ -1568,20 +1569,27 @@ public class ClerkDisbursementRequestApiResource { final BigDecimal amount; final String accountRef; // savings account number shown in GL comments; null for source/target GL final String txnId; // full Fineract transaction_id (e.g. "S123"); null for pure GL entries - final Long savingsAccountId; // numeric savings account ID for GL entity linking; null for GL-only entries + final Long savingsAccountId; // numeric savings OR current account ID for GL entity linking; null for GL-only entries + final boolean isCurrentAccount; // true if savingsAccountId names a row in m_current_account, not m_savings_account GlEntry(final Long glAccountId, final BigDecimal amount, final String accountRef, final String txnId) { - this(glAccountId, amount, accountRef, txnId, null); + this(glAccountId, amount, accountRef, txnId, null, false); } GlEntry(final Long glAccountId, final BigDecimal amount, final String accountRef, final String txnId, final Long savingsAccountId) { + this(glAccountId, amount, accountRef, txnId, savingsAccountId, false); + } + + GlEntry(final Long glAccountId, final BigDecimal amount, final String accountRef, final String txnId, + final Long savingsAccountId, final boolean isCurrentAccount) { this.glAccountId = glAccountId; this.amount = amount; this.accountRef = accountRef; this.txnId = txnId; this.savingsAccountId = savingsAccountId; + this.isCurrentAccount = isCurrentAccount; } } @@ -1603,8 +1611,16 @@ public class ClerkDisbursementRequestApiResource { /** * After posting a combined journal entry, links each debit (typeEnum=2) or credit (typeEnum=1) - * row to its savings account so the GL view shows ACCOUNT NO instead of "—". + * row to its savings OR current account so the GL view shows ACCOUNT NO instead of "—". * Rows are matched to entries by insertion order (Fineract inserts in JSON array order). + * + * m_current_account and m_savings_account each keep their own identity sequence, so the same + * numeric id can be a real row in both -- this used to tag every entry entity_type_enum=2 + * (PortfolioProductType.SAVING) regardless of which table the id actually named, so the GL view + * would resolve a current account's entry to a same-numbered savings account or vice versa. + * entity_type_enum=6 (PortfolioProductType.CURRENT) exists precisely so a current account's + * entries can be told apart from a savings account's; GlEntry.isCurrentAccount says which one + * this entry's id actually is, decided once already, where the account was first resolved. */ private void linkGlEntriesToSavingsAccounts(final String glTxnId, final List entries, final int typeEnum) { @@ -1618,11 +1634,15 @@ public class ClerkDisbursementRequestApiResource { Long.class, glTxnId, typeEnum, entries.size()); java.util.Collections.reverse(rowIds); for (int i = 0; i < Math.min(rowIds.size(), entries.size()); i++) { - final Long savingsAccId = entries.get(i).savingsAccountId; + final GlEntry entry = entries.get(i); + final Long savingsAccId = entry.savingsAccountId; if (savingsAccId != null) { + final int entityType = entry.isCurrentAccount + ? PortfolioProductType.CURRENT.getValue() + : PortfolioProductType.SAVING.getValue(); jdbcTemplate.update( - "UPDATE acc_gl_journal_entry SET entity_type_enum = 2, entity_id = ? WHERE id = ?", - savingsAccId, rowIds.get(i)); + "UPDATE acc_gl_journal_entry SET entity_type_enum = ?, entity_id = ? WHERE id = ?", + entityType, savingsAccId, rowIds.get(i)); } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/service/SponsorDDIssueRequestWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/service/SponsorDDIssueRequestWritePlatformServiceImpl.java index d263de1bc..54471b2db 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/service/SponsorDDIssueRequestWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/service/SponsorDDIssueRequestWritePlatformServiceImpl.java @@ -37,6 +37,7 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.sponsordd.data.SponsorDDBatchIssueResult; import org.apache.fineract.portfolio.sponsordd.data.SponsorDDSlipLine; import org.apache.fineract.portfolio.voucher.service.VoucherLine; @@ -602,6 +603,7 @@ public class SponsorDDIssueRequestWritePlatformServiceImpl implements SponsorDDI final Long drGlAccountId; Long linkedAccountId = null; + boolean linkedAccountIsCurrent = false; if (METHOD_ACCOUNT_DEBIT.equals(paymentMethod)) { final Long savingsAccountId = request.get("savings_account_id") != null ? ((Number) request.get("savings_account_id")).longValue() : null; final Long currentAccountId = request.get("current_account_id") != null ? ((Number) request.get("current_account_id")).longValue() : null; @@ -627,6 +629,7 @@ public class SponsorDDIssueRequestWritePlatformServiceImpl implements SponsorDDI issuingBranchId, issuedDate, currentUser.getId(), note); drGlAccountId = getCurrentControlGlId(currentAccountId); linkedAccountId = currentAccountId; + linkedAccountIsCurrent = true; } if (drGlAccountId == null) { throw new IllegalStateException("Could not resolve the control GL account for this account's product. Check the product's accounting (GL) mapping."); @@ -662,7 +665,8 @@ public class SponsorDDIssueRequestWritePlatformServiceImpl implements SponsorDDI final String comments = "Sponsor DD issuance - DD No " + ddNumber; final Long resultingJournalEntryId = postGlOffsetEntry(drGlAccountId, ddPayableGlAccountId, faceValue, chargesAmount, - chargesGlAccountId, gstAmount, gstGlAccountId, issuingBranchId, issuedDate, comments, linkedAccountId); + chargesGlAccountId, gstAmount, gstGlAccountId, issuingBranchId, issuedDate, comments, linkedAccountId, + linkedAccountIsCurrent); final CommandProcessingResult issueResult = this.sponsorDDWritePlatformService.issueDemandDraft( leafletId, clientId, walkinPurchaserName, walkinPurchaserAddress, walkinPurchaserAadhaarMasked, beneficiaryName, @@ -912,6 +916,7 @@ public class SponsorDDIssueRequestWritePlatformServiceImpl implements SponsorDDI Long resultingCurrentTransactionId = null; final Long drGlAccountId; Long linkedAccountId = null; + boolean linkedAccountIsCurrent = false; if (METHOD_ACCOUNT_DEBIT.equals(paymentMethod)) { final String note = "Sponsor DD slip issuance - DD Nos " + ddNumbersSummary; // Book debit, no journal entry of its own -- see approveIssueRequest(). @@ -925,6 +930,7 @@ public class SponsorDDIssueRequestWritePlatformServiceImpl implements SponsorDDI issuingBranchId, issuedDate, currentUser.getId(), note); drGlAccountId = getCurrentControlGlId(currentAccountId); linkedAccountId = currentAccountId; + linkedAccountIsCurrent = true; } if (drGlAccountId == null) { throw new IllegalStateException( @@ -946,7 +952,8 @@ public class SponsorDDIssueRequestWritePlatformServiceImpl implements SponsorDDI final String comments = "Sponsor DD slip issuance - DD Nos " + ddNumbersSummary; final Long resultingJournalEntryId = postGlOffsetEntry(drGlAccountId, ddPayableGlAccountId, totalFaceValue, totalCharges, - chargesGlAccountId, totalGst, gstGlAccountId, issuingBranchId, issuedDate, comments, linkedAccountId); + chargesGlAccountId, totalGst, gstGlAccountId, issuingBranchId, issuedDate, comments, linkedAccountId, + linkedAccountIsCurrent); for (final Map request : requests) { this.sponsorDDWritePlatformService.issueDemandDraft( @@ -1757,15 +1764,17 @@ public class SponsorDDIssueRequestWritePlatformServiceImpl implements SponsorDDI * liability, so they get their own credit leg rather than being folded into the liability). Resolves * the resulting row ID from acc_gl_journal_entry so it can be linked from m_sponsor_dd_issue_request / * m_sponsor_dd_transaction for audit traceability. When linkedAccountId is given (the client's - * savings/current account for an account-debit DD), the Dr-side row is tagged entity_type_enum=2 / - * entity_id=linkedAccountId so GL reports show the account number instead of a blank "—", matching - * how Fineract's own auto-generated entries display. + * savings/current account for an account-debit DD), the Dr-side row is tagged + * entity_type_enum=SAVING(2) or CURRENT(6) per linkedAccountIsCurrent / entity_id=linkedAccountId so + * GL reports show the account number instead of a blank "—", matching how Fineract's own + * auto-generated entries display -- and so the two id spaces (m_savings_account, m_current_account + * each with their own sequence) can't resolve a same-numbered stranger's account instead. */ private Long postGlOffsetEntry(final Long drGlAccountId, final Long crGlAccountId, final BigDecimal amount, final BigDecimal chargesAmount, final Long chargesGlAccountId, final Long officeId, final LocalDate txnDate, final String comments, final Long linkedAccountId) { return postGlOffsetEntry(drGlAccountId, crGlAccountId, amount, chargesAmount, chargesGlAccountId, null, null, officeId, - txnDate, comments, linkedAccountId); + txnDate, comments, linkedAccountId, false); } /** @@ -1811,7 +1820,8 @@ public class SponsorDDIssueRequestWritePlatformServiceImpl implements SponsorDDI /** As above, with an extra optional GST credit leg (GST is charged on chargesAmount only). */ private Long postGlOffsetEntry(final Long drGlAccountId, final Long crGlAccountId, final BigDecimal amount, final BigDecimal chargesAmount, final Long chargesGlAccountId, final BigDecimal gstAmount, final Long gstGlAccountId, - final Long officeId, final LocalDate txnDate, final String comments, final Long linkedAccountId) { + final Long officeId, final LocalDate txnDate, final String comments, final Long linkedAccountId, + final boolean linkedAccountIsCurrent) { final boolean hasCharges = chargesAmount != null && chargesAmount.compareTo(BigDecimal.ZERO) > 0; final boolean hasGst = gstAmount != null && gstAmount.compareTo(BigDecimal.ZERO) > 0; @@ -1872,11 +1882,18 @@ public class SponsorDDIssueRequestWritePlatformServiceImpl implements SponsorDDI // posted row anyway (ORDER BY id DESC), as defense-in-depth against the id text ever // being reused (e.g. after a sequence reset) rather than trusting uniqueness implicitly. if (linkedAccountId != null) { + // m_current_account and m_savings_account each keep their own identity sequence, so the same + // numeric id can be a real row in both -- entity_type_enum must say which one linkedAccountId + // names, not assume SAVING(2) always (see linkedAccountIsCurrent's callers: the account-debit + // branch resolves either a savingsAccountId or a currentAccountId into this same parameter). + final int entityType = linkedAccountIsCurrent + ? PortfolioProductType.CURRENT.getValue() + : PortfolioProductType.SAVING.getValue(); this.jdbcTemplate.update( - "UPDATE acc_gl_journal_entry SET entity_type_enum = 2, entity_id = ? WHERE id = (" + "UPDATE acc_gl_journal_entry SET entity_type_enum = ?, entity_id = ? WHERE id = (" + "SELECT id FROM acc_gl_journal_entry WHERE transaction_id = ? AND type_enum = 2 AND reversed = false" + " ORDER BY id DESC LIMIT 1)", - linkedAccountId, transactionId); + entityType, linkedAccountId, transactionId); } final List> rows = this.jdbcTemplate.queryForList(