feat(savings,shares): bidirectional savings/share account linking, optional savings link on share create

- Savings account creation validates and stores a required "shareAccountId" once Share Linking is
  enabled, checked against the client's own active share accounts (raw SQL - fineract-savings has
  no dependency on the shareaccounts domain). On save, mirrors the link back onto the share
  account's own savings_account_id, the same way that field already gets set from the share side.
- New GET /clients/{id}/activeshareaccounts - a client's active share accounts, for the savings
  creation screen's picker (kept separate from the shared Savings/FD/RD/Current template DTO).
- ShareAccountDataSerializer.validateAndCreate no longer requires savingsAccountId on create - the
  DB column and domain relationship were already nullable, only this validation blocked it.

fix(branchday): stop the denomination carry-forward preview from doubling yesterday's closing count

GET /branchday/denominations classified every detail row that wasn't literally leg='OPENING' as
"closing" - which, once CLOSING_VAULT rows existed alongside CLOSING (single-teller offices record
the same breakdown under both), silently doubled every count shown as "yesterday's closing" when
opening the next business day. Filters to leg IN ('OPENING','CLOSING') explicitly instead of an
else-catches-everything branch.

feat(accounting): seed missing Income/Expense/Transfers-Suspense/Escheat-Liability accounts

The Basoda-derived chart of accounts (0523) carried none of these - a balance sheet has no P&L
heads, and Transfers Suspense/Escheat Liability aren't balance-sheet line items either - so a
Savings Product's accounting mapping had nowhere to point those required fields. Same guard as
0523: only seeds a Basoda-derived tenant that hasn't already gotten it, never touches a tenant with
its own chart of accounts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-31 22:17:54 +05:30
co-authored by Claude Sonnet 5
parent 509370eaeb
commit dc0539d53d
14 changed files with 424 additions and 12 deletions
@@ -48,7 +48,8 @@ public class SavingsAccountConstant extends SavingsApiConstants {
introducerIdParamName, introducerNameParamName, introducerCifNumberParamName,
nomineeParamName, "nominees", linkedAccountIdParamName, linkedAccountTypeParamName, linkedAccountNoParamName,
accountFacilityConfigParamName, jointHoldersParamName,
balanceSwapOptionParamName, msmeParamName, autoDebitFacilityParamName, introducerRequiredParamName, shareLinkingEnabledParamName));
balanceSwapOptionParamName, msmeParamName, autoDebitFacilityParamName, introducerRequiredParamName, shareLinkingEnabledParamName,
shareAccountIdParamName));
/**
* These parameters will match the class level parameters of {@link SavingsAccountData}. Where possible, we try to
@@ -4426,6 +4426,16 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
@Column(name = "share_linking_enabled", nullable = false, columnDefinition = "boolean default false")
private Boolean shareLinkingEnabled = false;
/**
* Plain id, not a JPA relationship - fineract-savings cannot depend on the shareaccounts domain
* class (it lives in fineract-provider, which depends on fineract-savings, not the other way
* round). The write side resolves and validates this id against m_share_account directly; this
* column just carries it back to the share account's own {@code savings_account_id}, mirroring
* the link from both ends.
*/
@Column(name = "share_account_id")
private Long shareAccountId;
@Column(name = "nomination_facility", nullable = false, columnDefinition = "boolean default false")
private Boolean nominationFacility = false;
@@ -4532,6 +4542,9 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
public Boolean getShareLinkingEnabled() { return shareLinkingEnabled; }
public void setShareLinkingEnabled(Boolean v) { this.shareLinkingEnabled = v; }
public Long getShareAccountId() { return shareAccountId; }
public void setShareAccountId(Long v) { this.shareAccountId = v; }
public boolean isSeniorCitizen() { return isSeniorCitizen; }
public void setSeniorCitizen(boolean v) { this.isSeniorCitizen = v; }
@@ -53,4 +53,5 @@
<include file="parts/2029_remove_proposed_payload_json.xml" relativeToChangelogFile="true" />
<include file="parts/2030_add_savings_account_pending_edit_table.xml" relativeToChangelogFile="true" />
<include file="parts/2031_add_savings_account_edit_approval_permissions.xml" relativeToChangelogFile="true" />
<include file="parts/2032_add_share_account_id_to_savings_account.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- The reverse of m_share_account.savings_account_id (0489-era, already existed): this is the
savings account's own pointer back to the share account it was linked to at creation, when
Share Linking Enabled was set. No FK to m_share_account here - fineract-savings has no
reference to that table's owning module, so the write side is responsible for validating
this id and for keeping the two ends of the link in sync. -->
<changeSet id="2032_add_share_account_id_to_savings_account" author="cbs-nitesh">
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_savings_account"/>
<not><columnExists tableName="m_savings_account" columnName="share_account_id"/></not>
</preConditions>
<addColumn tableName="m_savings_account">
<column name="share_account_id" type="BIGINT"/>
</addColumn>
</changeSet>
</databaseChangeLog>