From 23ee4f1063ac5729e84aee52c47379714444b93a Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Wed, 23 Sep 2026 14:20:55 +0530 Subject: [PATCH] fix(deposits): Deposit Account Link wrongly demanded a co-op share account SYMPTOM Creating an FD/RD account with "Deposit Account Link" enabled and a Savings/Current account correctly picked in the UI still failed with: "A share account must be chosen when Share Linking is enabled." even though no share account field exists anywhere on the FD/RD form. ROOT CAUSE SavingsAccountAssembler.applyCbsFields() has one boolean flag, shareLinkingEnabled, serving two unrelated features that got collided together (traceable to commit dc0539d53, which repurposed the flag for co-op Share Account linking on plain Savings products): - On a plain Savings product: "Share Linking" = link a co-op m_share_account (shareAccountId). - On an FD/RD product: the SAME flag is literally labeled "Deposit Account Link" on the product screen (fixed-deposit-product-details-step.component.html) and is meant to require the pre-existing, native linkedAccountId field (already wired end-to-end: SavingsAccountAssembler sets it a few lines below this check, the FD "Maturity Instructions: Transfer to Savings" flow already depends on it, DepositAccountReadPlatformServiceImpl already reads it back for display). Before dc0539d53, 7 real historical FD accounts (ids 51-57 in fineract_default) prove linkedAccountId was the correct, working mechanism for this. The block added by that commit made shareAccountId mandatory unconditionally, for every product, breaking FD/RD. FIX SavingsAccountAssembler.applyCbsFields(): when shareLinkingEnabled is true, branch on account.depositAccountType(): - FIXED_DEPOSIT / RECURRING_DEPOSIT: require linkedAccountId (the field the FD/RD form already sends) instead of shareAccountId. - everything else (plain Savings): unchanged, still requires and validates shareAccountId against m_share_account. SCOPE Single method, single file. No new column, no new flag, no frontend change - the frontend's Deposit Account Link picker was already correct; only this one validation block was reading the wrong field for FD/RD. IF THIS BREAKS SOMETHING LATER - "A share account must be chosen..." reappearing for FD/RD -> check account.depositAccountType() is resolving to FIXED_DEPOSIT/ RECURRING_DEPOSIT at this point (it's a simple override per account subclass, see FixedDepositAccount#depositAccountType()). - A plain Savings product's genuine co-op Share Linking breaking -> that path (the `else` branch here) is untouched; if it breaks, the regression is elsewhere. - A much larger version of this fix (a whole new depositAccountLinkRequired flag decoupled from shareLinkingEnabled, ~18 files) was attempted earlier in the same session and fully reverted at the user's request after repeated Gradle build-cache issues made it impossible to verify was even running; this is the intentionally minimal replacement. Co-Authored-By: Claude Sonnet 5 --- .../domain/SavingsAccountAssembler.java | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java index 8453b6e92..22cef0438 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java @@ -414,20 +414,33 @@ public class SavingsAccountAssembler { // dependency on the shareaccounts domain class) rather than trusted blindly, the same way an // account number typed for a linked savings/loan account elsewhere in this form is verified // before being accepted. + // + // For a term deposit (FD/RD), the product screen labels this same flag "Deposit Account Link" -- + // it links an existing Savings/Current account (linkedAccountId, set below), not a co-op share + // account, so it's validated against that field instead of shareAccountId. if (Boolean.TRUE.equals(account.getShareLinkingEnabled())) { - final Long shareAccountId = this.fromApiJsonHelper.extractLongNamed(shareAccountIdParamName, element); - if (shareAccountId == null) { - throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.share.account.required", - "A share account must be chosen when Share Linking is enabled."); + final boolean isTermDeposit = account.depositAccountType().isFixedDeposit() || account.depositAccountType().isRecurringDeposit(); + if (isTermDeposit) { + final Long linkedAccountIdForCheck = this.fromApiJsonHelper.extractLongNamed(linkedAccountIdParamName, element); + if (linkedAccountIdForCheck == null) { + throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.linked.account.required", + "A Savings/Current account must be linked when Deposit Account Link is enabled."); + } + } else { + final Long shareAccountId = this.fromApiJsonHelper.extractLongNamed(shareAccountIdParamName, element); + if (shareAccountId == null) { + throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.share.account.required", + "A share account must be chosen when Share Linking is enabled."); + } + final Integer matches = this.jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM m_share_account WHERE id = ? AND client_id = ? AND status_enum = 300", Integer.class, + shareAccountId, account.clientId()); + if (matches == null || matches == 0) { + throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.share.account.not.active.for.client", + "Share account " + shareAccountId + " is not an active share account for this client."); + } + account.setShareAccountId(shareAccountId); } - final Integer matches = this.jdbcTemplate.queryForObject( - "SELECT COUNT(*) FROM m_share_account WHERE id = ? AND client_id = ? AND status_enum = 300", Integer.class, - shareAccountId, account.clientId()); - if (matches == null || matches == 0) { - throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.share.account.not.active.for.client", - "Share account " + shareAccountId + " is not an active share account for this client."); - } - account.setShareAccountId(shareAccountId); } // Senior citizen rate — runs after validateDomainRules so it is the final persisted value