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
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:
+16
-11
@@ -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 ")
|
||||
|
||||
+13
-3
@@ -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
|
||||
|
||||
+21
-7
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-4
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
-13
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-9
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user