feat(dds): DDS product accounting tab; DDS posts only through product GLs

- DDS product names its own Customer Deposits, Interest Expense and
  Agent Commission Expense GLs (validated by kind), shown on the product
  view and in the approval review with old -> new on edits
- DdsLedger posts every DDS movement through the party's product GLs;
  commission is shared over the products an agent collected for
- dds-* settings removed from Global Configurations (0620); scheme
  accounts recognised by the DDS party linked to them, not a product id
- 0621 drops the agent cash, matured payable and current control GL
  columns, which nothing posts to
- Approval preview of interest/commission runs shows the product GLs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 12:43:41 +05:30
co-authored by Claude Opus 5.5
parent 6e89217e59
commit 422ea459be
18 changed files with 635 additions and 189 deletions
@@ -55,6 +55,16 @@ public final class DdsAccountTypeData implements Serializable {
/** The scheme's annual interest rate, which an account opened on it takes. */
private final BigDecimal interestRate;
private final Boolean active;
/** The product's own ledger accounts (see DdsAccountType), each with its code and name to show it by. */
private final Long depositLiabilityGlId;
private final String depositLiabilityGlCode;
private final String depositLiabilityGlName;
private final Long interestExpenseGlId;
private final String interestExpenseGlCode;
private final String interestExpenseGlName;
private final Long commissionExpenseGlId;
private final String commissionExpenseGlCode;
private final String commissionExpenseGlName;
private final LocalDateTime createdDate;
private final LocalDateTime lastModifiedDate;
}
@@ -115,6 +115,24 @@ public class DdsAccountType extends AbstractAuditableCustom {
@Column(name = "is_active", nullable = false)
private boolean active;
/**
* The ledger accounts this product posts through, chosen on its Accounting tab the way a savings
* product chooses its own. DDS posts through these alone - there is no bank-wide account to fall
* back on - so a product missing one stops its postings with a message naming what to set.
*
* What the bank owes this product's customers - DDS Customer Deposits, or the product's own.
*/
@Column(name = "deposit_liability_gl_id")
private Long depositLiabilityGlId;
/** What interest paid to this product's customers costs the bank. */
@Column(name = "interest_expense_gl_id")
private Long interestExpenseGlId;
/** What the agents' commission on collecting for this product costs the bank. */
@Column(name = "commission_expense_gl_id")
private Long commissionExpenseGlId;
protected DdsAccountType() {
//
}
@@ -131,6 +149,9 @@ public class DdsAccountType extends AbstractAuditableCustom {
this.prematureInterestReduction = valueOr(command, "prematureInterestReduction", DEFAULT_INTEREST_REDUCTION);
this.prematureAgentCommissionPenalty = valueOr(command, "prematureAgentCommissionPenalty", DEFAULT_AGENT_PENALTY);
this.interestRate = command.bigDecimalValueOfParameterNamed("interestRate");
this.depositLiabilityGlId = command.longValueOfParameterNamed("depositLiabilityGlId");
this.interestExpenseGlId = command.longValueOfParameterNamed("interestExpenseGlId");
this.commissionExpenseGlId = command.longValueOfParameterNamed("commissionExpenseGlId");
// A newly created account type is available for use straight away.
this.active = !command.parameterExists("active") || command.booleanPrimitiveValueOfParameterNamed("active");
}
@@ -238,6 +259,23 @@ public class DdsAccountType extends AbstractAuditableCustom {
this.interestRate = newValue;
}
// The product's ledger accounts. All three are required; the deserializer refuses a null.
if (command.parameterExists("depositLiabilityGlId")
&& command.isChangeInLongParameterNamed("depositLiabilityGlId", this.depositLiabilityGlId)) {
this.depositLiabilityGlId = command.longValueOfParameterNamed("depositLiabilityGlId");
actualChanges.put("depositLiabilityGlId", this.depositLiabilityGlId);
}
if (command.parameterExists("interestExpenseGlId")
&& command.isChangeInLongParameterNamed("interestExpenseGlId", this.interestExpenseGlId)) {
this.interestExpenseGlId = command.longValueOfParameterNamed("interestExpenseGlId");
actualChanges.put("interestExpenseGlId", this.interestExpenseGlId);
}
if (command.parameterExists("commissionExpenseGlId")
&& command.isChangeInLongParameterNamed("commissionExpenseGlId", this.commissionExpenseGlId)) {
this.commissionExpenseGlId = command.longValueOfParameterNamed("commissionExpenseGlId");
actualChanges.put("commissionExpenseGlId", this.commissionExpenseGlId);
}
if (command.isChangeInBooleanParameterNamed("active", this.active)) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("active");
actualChanges.put("active", newValue);