fix(accounting): allow unbalanced legs when defining opening balances
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 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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 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 E2E Tests / E2E Tests (Shard 1 of 10) (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 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (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-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 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

Defining opening balances was rejected by the debit/credit balance
check that ordinary journal entries go through. For opening balances
that check does not hold: every leg is posted against the opening
balance contra account, so the books always balance, and the submitted
legs legitimately differ by the opening balance of the locked,
denomination-managed cash account, which is already posted and can
never be part of the request.

- validateBusinessRulesForJournalEntries takes a requireBalanced flag.
  Ordinary journal entries keep the full check (requireBalanced = true).
- defineOpeningBalance passes false: it still needs at least one leg
  and an account and amount on every leg, but no longer requires debits
  and credits to be present on both sides or to add up.

Verified with :fineract-provider:compileJava.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 11:00:39 +05:30
co-authored by Claude Opus 5.5
parent 0afcf059a8
commit 539f6cd0f4
@@ -687,6 +687,16 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
}
private void validateBusinessRulesForJournalEntries(final JournalEntryCommand command) {
validateBusinessRulesForJournalEntries(command, true);
}
/**
* @param requireBalanced
* false for opening balances: every leg is posted against the contra account, so the books always
* balance, and the submitted legs legitimately differ by the locked (denomination-managed) cash
* account's opening balance that is already posted and can never be part of this request.
*/
private void validateBusinessRulesForJournalEntries(final JournalEntryCommand command, final boolean requireBalanced) {
// check if date of Journal entry is valid
final LocalDate transactionDate = command.getTransactionDate();
// shouldn't be in the future
@@ -706,6 +716,26 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
final SingleDebitOrCreditEntryCommand[] credits = command.getCredits();
final SingleDebitOrCreditEntryCommand[] debits = command.getDebits();
if (!requireBalanced) {
final List<SingleDebitOrCreditEntryCommand> entries = new ArrayList<>();
if (credits != null) {
entries.addAll(Arrays.asList(credits));
}
if (debits != null) {
entries.addAll(Arrays.asList(debits));
}
if (entries.isEmpty()) {
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.NO_DEBITS_OR_CREDITS, null, null, null);
}
for (final SingleDebitOrCreditEntryCommand entry : entries) {
if (entry.getAmount() == null || entry.getGlAccountId() == null) {
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null,
null);
}
}
return;
}
// atleast one debit or credit must be present
if (credits == null || credits.length == 0 || debits == null || debits.length == 0) {
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.NO_DEBITS_OR_CREDITS, null, null, null);
@@ -783,7 +813,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
final Office office = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
final String currencyCode = command.stringValueOfParameterNamed(JournalEntryJsonInputParams.CURRENCY_CODE.getValue());
validateBusinessRulesForJournalEntries(journalEntryCommand);
validateBusinessRulesForJournalEntries(journalEntryCommand, false);
validateNoDenominationManagedCashAccounts(journalEntryCommand);
/**