fix(dds): approval of a calculation run shows the entries its posting will book
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Waiting to run
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Waiting to run
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Waiting to run
Fineract Documentation build / build (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Waiting to run
Fineract Liquibase Only mode - PostgreSQL / test (push) Waiting to run

- A calculation run books nothing on the ledger, yet its approval preview was built as a
  posting and showed "Agent Commission Expense not set on the DDS product". The preview now
  projects the entries the matching Posting run will make, marked projected
- A commission calculation's split over product Commission Expense GLs is taken from the
  agent's collections no run has counted yet, up to the run's end date

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 22:04:48 +05:30
co-authored by Claude Sonnet 5.5
parent bb492c82b5
commit 375c2c95b6
2 changed files with 37 additions and 2 deletions
@@ -508,6 +508,28 @@ public class DdsLedger {
if (basis.isEmpty()) {
basis = this.jdbcTemplate.queryForList(String.format(COMMISSION_BASIS_SQL, ""), agentId, payingRunId); // NOSONAR
}
return shareOut(basis, amount);
}
/** The collections a commission run would count if it were posted now: not counted by any run yet, up to a date. */
private static final String COMMISSION_PENDING_SQL = "select upper(trim(p.sub_type)) as \"product\", t.commission_expense_gl_id as \"glId\", "
+ "sum(pt.amount) as \"volume\" from m_dds_party_transaction pt join m_dds_party p on p.id = pt.party_id "
+ "left join m_dds_account_type t on t.account_type_code = upper(trim(p.sub_type)) "
+ "where pt.agent_id = ? and pt.transaction_type = 'PARTY_TRAN' and pt.direction = 'CREDIT' "
+ "and pt.commission_batch_run_id is null and pt.transaction_date <= ? group by upper(trim(p.sub_type)), "
+ "t.commission_expense_gl_id order by upper(trim(p.sub_type))";
/**
* Where an agent's commission will be booked once it is posted - for a calculation run, which books
* nothing itself but whose approval should say which commission accounts the Posting run will debit.
* The split is over the collections the calculation counts (none counted yet, up to its end date).
*/
public Map<GLAccount, BigDecimal> projectedCommissionSplit(final Long agentId, final BigDecimal amount, final LocalDate through) {
return shareOut(this.jdbcTemplate.queryForList(COMMISSION_PENDING_SQL, agentId, java.sql.Date.valueOf(through)), amount); // NOSONAR
}
/** Shares an amount over the commission accounts of the products collected for, by volume; the last takes the rounding. */
private Map<GLAccount, BigDecimal> shareOut(final List<Map<String, Object>> basis, final BigDecimal amount) {
if (basis.isEmpty()) {
throw new DdsLedgerNotConfiguredException("a collection by this agent to tell which DDS product's Agent Commission Expense "
+ "account the commission belongs to");
@@ -582,13 +582,22 @@ public class DdsApprovalReadService {
// customer's product's interest account, commission shared over the commission accounts
// of the products each agent collected for, as the posting itself will book it.
final String jobType = first("select job_type from m_dds_batch_run where id = ?", resourceId);
// A calculation books nothing itself; its lines are what the matching Posting run will
// book, shown so the checker sees which accounts and ledger accounts the figures belong to.
final boolean projected = jobType != null && jobType.endsWith("_CALCULATION");
if (jobType != null && jobType.startsWith("COMMISSION")) {
final Map<String, BigDecimal> byAccount = new LinkedHashMap<>();
final Map<String, String> names = new LinkedHashMap<>();
for (final Map<String, Object> runRow : this.jdbcTemplate.queryForList( // NOSONAR
"select agent_id, amount from m_dds_batch_run_row where batch_run_id = ? and amount > 0 order by id", resourceId)) {
"select agent_id, amount, to_date from m_dds_batch_run_row where batch_run_id = ? and amount > 0 order by id",
resourceId)) {
try {
this.ddsLedger.commissionSplit(((Number) runRow.get("agent_id")).longValue(), (BigDecimal) runRow.get("amount"), resourceId)
final long agentOfRow = ((Number) runRow.get("agent_id")).longValue();
final BigDecimal amountOfRow = (BigDecimal) runRow.get("amount");
(projected && runRow.get("to_date") != null
? this.ddsLedger.projectedCommissionSplit(agentOfRow, amountOfRow,
((java.sql.Date) runRow.get("to_date")).toLocalDate())
: this.ddsLedger.commissionSplit(agentOfRow, amountOfRow, resourceId))
.forEach((account, share) -> {
byAccount.merge(account.getGlCode(), share, BigDecimal::add);
names.put(account.getGlCode(), account.getName());
@@ -622,6 +631,10 @@ public class DdsApprovalReadService {
lines.add(line("CR", (String) credited.get("accountNo"), (String) credited.get("accountName"),
(String) credited.get("type"), (BigDecimal) credited.get("amount")));
}
if (projected) {
// Marked, so the review says these entries are made by the Posting run, not on approval.
lines.forEach(each -> each.put("projected", true));
}
}
default -> {
// A change to a party, agent or scheme moves no money and has no sides.