fix(accounting): tag current-account GL entries entity_type_enum=CURRENT instead of reusing SAVING, everywhere this codebase links an entry to its account
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s

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 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 10:34:39 +05:30
co-authored by Claude Sonnet 5
parent 1e903232f0
commit 87f3e591ce
6 changed files with 123 additions and 47 deletions
@@ -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 ")
@@ -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
@@ -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));
}
}
}
@@ -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));
}
}
}
@@ -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<GlEntry> 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));
}
}
}
@@ -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<String, Object> 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<Map<String, Object>> rows = this.jdbcTemplate.queryForList(