Compare commits

...
95 Commits
Author SHA1 Message Date
niteshandClaude Sonnet 5.5 da1e21f2ca feat(security): block undo/reverse of posted transactions at API level
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 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 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-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
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (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 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 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 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
Rejects journal entry reverse, savings/current undo and reverse, fixed and recurring deposit
undo, and loan transaction undo (adjust with zero amount) with a clear error, so posted entries
cannot be reversed by calling the API directly.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 18:42:01 +05:30
niteshandClaude Sonnet 5.5 b1eea68b17 feat(cashier): cashier-only cash operations, exchange PAN/denomination checks and branch position API
- Cash exchange: customer name/mobile/PAN (PAN mandatory above Rs 50,000), denomination validation
  against m_denomination_master, cashier required; Liquibase 0599 adds the three columns.
- Inter-cashier: sender must be the logged-in cashier, cash-allocated validation, new
  cashier-summary endpoint.
- Reconciliation restricted to the logged-in cashier's own records.
- New cashreport/branch-position endpoint (vault + cashier drawers, denomination-wise).
- Customer search returns the KYC PAN.
- Reverse endpoints removed from cash transfer, settlement, exchange and inter-cashier.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 18:42:00 +05:30
niteshandClaude Opus 5.5 539f6cd0f4 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>
2026-10-13 11:00:39 +05:30
niteshandClaude Opus 5.5 0afcf059a8 Merge latest feature/advanceLoanProduct into feature/merge-dds-advance-loan-into-dev-latest
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 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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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 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 Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (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 - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (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-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-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
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-5) (push) Canceled after 0s
Brings Sonali's four new commits (7-12 Oct):
- Loans A/C extension fields (collateral, eligibility, repayment, rate
  source, prepayment)
- Maker-checker for Delinquency Ranges and Buckets
- Loan interest rate register product master and GL account seeding
- Her merge of dev-latest (7 Oct)

Conflict resolved:
- changelog-tenant.xml: kept both. 0594_loan_rate_register_product_
  master, 0595_seed_loan_rate_register_gl_accounts,
  0596_enable_delinquency_maker_checker and 0597_add_loan_product_
  loans_ext_fields go with the earlier advance loan changelogs, before
  the DDS block that stays last. No file name is shared with
  dev-latest's 0594-0597 files.

0581_enable_loan_rate_register_maker_checker.xml: taken as on
feature/advanceLoanProduct. It edits the existing 0581a/0581b
changesets (CBSLOANBASEINTERESTRATE -> LOANBASEINTERESTRATE) and adds
0581c to rename rows already seeded; all three carry
<validCheckSum>ANY</validCheckSum>, so databases that ran the old
version still validate.

CommandSourceService: Sonali's LOANBASEINTERESTRATE, DELINQUENCY_BUCKET
and DELINQUENCY_RANGE checks sit alongside the DDS isDdsEntity check.

Nothing from dev-latest, DDS or Sonali is removed; no file deleted; no
DB password change. Verified with :fineract-provider:compileJava.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:29:42 +05:30
sonaliandClaude Sonnet 5 2d10ec60f8 feat(loan-products): add Loans A/C extension fields (collateral, eligibility, repayment, rate source, prepayment)
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 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 7 of 10) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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 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 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 - 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
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 Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (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
Stores the Loans A/C (EMI) wizard's fields that have no home in
Fineract's core loan-product schema on the same per-product satellite
used for Advances: collateral/guarantor rules, borrower eligibility,
disbursement, repayment mode, rate source / floating reset, and
part-prepayment/foreclosure. Columns are loan_-prefixed so they never
collide with the Advances columns on the shared table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-12 15:50:47 +05:30
sonaliandClaude Sonnet 5 14973dfd5e feat(delinquency): add maker-checker for Delinquency Ranges and Buckets
Range/bucket create, update and delete now route through the standard
command-source approval pipeline, since a bucket drives the SMA/NPA
classification of every loan product linked to it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-12 15:50:29 +05:30
niteshandClaude Opus 5.5 f96705baa4 Merge branch 'feature/advanceLoanProduct' into merge/dds-advance-loan-into-dev-latest
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 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 Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (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 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 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 - MySQL / test (test-core-1) (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-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
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
Adds the Advance Loan Product backend on top of the DDS merge of
dev-latest:
- Advance loan product ext entity, API and Liquibase
  (LoanProductAdvanceExtService; loan product API accepts and returns
  "advanceExtensions")
- Fees-Penalty-Interest-Principal repayment transaction processor,
  enabled by fineract.loan.transactionprocessor.
  fees-penalties-interest-principal.enabled (default true)
- Loan interest rate register backend

Conflict resolved:
- changelog-tenant.xml: kept both. The four advance loan changelogs
  (0571_create_loan_product_advance_ext, 0572_add_loan_product_advance_
  ext_fields, 0580_create_loan_rate_register_tables,
  0581_enable_loan_rate_register_maker_checker) go just before the DDS
  block, which its comment says stays last. Their numbers overlap
  dev-latest files but no file name is shared.

Not taken from feature/advanceLoanProduct:
- Commit b72657703 changed the default DB password in
  application.properties to a UAT database password. That merged
  without a conflict; the dev-latest password lines are kept so local
  setups keep working and the UAT password does not go into the
  repository. The new transaction processor property from the same
  file is kept.

Nothing from dev-latest or DDS is removed: no file deleted; only
LoanProductsApiResource (one supported parameter added),
LoanProductConfiguration (one constructor argument added) and
application.properties change existing lines. CommandWrapperBuilder,
changed on both sides, has no duplicate methods.

Verified with :fineract-provider:compileJava.

Branch renamed from merge/dds-into-dev-latest, since it now carries
both DDS and Advance Loan Product.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:29:13 +05:30
niteshandClaude Opus 5.5 9d6ef18929 Merge branch 'feature/dds' into merge/dds-into-dev-latest
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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 - MySQL / test (test-core-1) (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 - MariaDB / test (test-core-5) (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-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
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
Brings the DDS (Daily Deposit Scheme) backend from feature/dds onto the
current dev-latest:
- Agent settlement (Agent Tran), Close Party account, reports
  (Supplementary-DDS, Master Data Export, Close Register), Party-Savings
  link, savings product category field
- Collection entries held before posting, per-agent GL accounts,
  maturity auto close
- Vouchers, maker-checker approvals, payout & close, Sundry GL, closure
  tab and fixes
- POST_DDS_INTEREST job

No content conflicts. No dev-latest file is removed; only three
dev-latest lines are extended with a DDS condition:
- CommandSourceService: DDS entities join ROLE in the maker-checker
  entity check (isDdsEntity).
- PortfolioCommandSourceWritePlatformServiceImpl: a DDS entry can never
  be approved by the user who made it, even a checker super user. The
  rule for every other entity is unchanged.
- SavingsProductDataValidator: productCategory accepted as a savings
  product parameter.

Files changed on both branches and auto-merged: CommandWrapperBuilder,
CommandSourceService, JobName, SavingsApiConstants, SavingsProductData,
SavingsProduct, FixedDepositProductData, RecurringDepositProductData,
DepositProductReadPlatformServiceImpl. Checked for duplicate methods,
fields and enum names.

Liquibase: 55 DDS changelogs (0470-0525) included as one block at the
end of changelog-tenant.xml. Their numbers overlap dev-latest's files
but no file name is shared, and Liquibase tracks changesets by file
path, so nothing clashes. They keep their names so databases that
already ran them do not run them again. No permission code, job name
or configuration name is inserted by both branches.

Verified with :fineract-provider:compileJava. The full compileJava
fails only in fineract-client (generated CKYC, committee meeting and
election APIs), which feature/dds does not touch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:11:14 +05:30
niteshandClaude Opus 5.5 936e21dcf6 Merge branch 'feature/fd-interest-posting' into dev-latest
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (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 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 1 of 10) (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 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 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-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (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-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
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
Brings the FD interest posting work into dev-latest:
- FD interest posting approval with pooled GL postings, posting period
  shown per line, maturity postings surfaced, double-posting guard
- FD maturity approval report added and then reverted (maturity now
  runs directly through the scheduler, no approval step)
- Pending user approval encodes the password and emails it
- New-tenant user personalisation, with the 'system' username kept so
  scheduler jobs and COB can still find the system user

Conflicts resolved:
- SavingsAccountReadPlatformServiceImpl#retrieveAllSavingsDataForInterestPosting:
  kept both exclusions. Recurring deposits (deposit_type_enum 300) stay
  out of the generic interest posting job (dev-latest), and fixed
  deposits are also left out when FD interest posting approval is
  enabled (feature branch), so FD interest is not paid twice.
  Comments from both sides merged.
- final-changelog-tenant.xml: kept dev-latest's 0592-0598 TDS and
  recurring deposit changelogs and added
  0593_fd_interest_posting_approval.xml after them. The file keeps its
  name although its number clashes with 0593_tds_...: Liquibase tracks
  changesets by file path, so renaming it would re-run it on databases
  that already applied it. Changeset ids in the two files differ.

Verified with :fineract-provider:compileJava.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 12:23:55 +05:30
niteshandClaude Opus 5.5 aceea89c5c fix(tenants): keep the 'system' username when provisioning a new tenant
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (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 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 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-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-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
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
Problem
Commit 3d3ce92e9 personalised the three users seeded into a brand-new
tenant database and renamed user id 2 from 'system' to
'<tenant>_system' (for example 'default_system'). Fineract looks this
user up by the fixed name AppUserConstants.SYSTEM_USER_NAME ('system')
through AppUserRepositoryWrapper.fetchSystemUser(), so on every fresh
tenant:
- every scheduler job failed once a minute with
  "UserNotFoundException: User with name 'system' does not exist"
  (seen with the Send Asynchronous Events job);
- COB initialisation, savings escheat, current account closure and the
  stuck-job listener would fail in the same way.
Existing tenants were not affected because the provisioner only runs
right after the very first migration of a new tenant.

Fix
- TenantInitialUserProvisioner now sets user id 2's username to
  AppUserConstants.SYSTEM_USER_NAME and only replaces its password.
- Admin and interop users are still personalised as
  '<tenant>_admin' and '<tenant>_interop'.
- Javadoc and the startup log line now say the system user keeps its
  name and why.

Resulting users on a new tenant
- <tenant>_admin    (password: initial-user-password or built-in default)
- system            (same password, name unchanged)
- <tenant>_interop  (same password)

Tenants already created with '<tenant>_system' need a one-off fix:
  UPDATE m_appuser SET username = 'system' WHERE id = 2;

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:55:18 +05:30
keshav 99ef5edfd8 Merge branch 'dev-latest' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend into dev-latest
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract Documentation build / build (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 E2E Tests / E2E Tests (Shard 3 of 10) (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 E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (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 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 Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (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-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 - PostgreSQL / test (test-core-1) (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
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
2026-10-12 10:25:19 +05:30
keshav 9df8e924a1 feat: recurring deposit product rules, lien, auto debit, missed installment penalty, and jobs 2026-10-12 10:25:12 +05:30
niteshandClaude Sonnet 5 1fd94615a9 revert(fixed-deposits): remove FD maturity approval report
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 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 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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
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 Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (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 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 - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (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 Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (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
A fixed deposit's maturity is settled directly by the nightly
updateMaturityDetails job again, unconditionally, for every deposit
type and every closure instruction — the approval gate this introduced
turned out to risk more than it protected and is being removed before
it ever shipped enabled by default.

Removes the fdmaturity package entire (report service, orchestrator,
API resource, noon-generation job), its migration and global
configuration, the day-close integration in BranchDayRegisterApiResource,
and FixedDepositAccount#previewMaturityInterestPeriods, which existed
only for this report's preview and has no other caller.

The periodic FD interest posting approval report (0593) is untouched —
a matured deposit's final interest, paid directly by the nightly job,
already surfaces there under "Posted outside approval", same as it did
before this report existed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-09 12:50:11 +05:30
niteshandClaude Sonnet 5 3d3ce92e93 feat(tenants): personalise seeded user credentials for a brand-new tenant
Adds TenantInitialUserProvisioner: right after the very first Liquibase
migration of a tenant that was never on Flyway, renames the three
seeded users (ids 1-3, otherwise shipping as mifos/system/interopUser
with the public default password) to <tenantIdentifier>_admin/_system/
_interop, with a password from fineract.tenant.initial-user-password
or a built-in default. An existing (Flyway-era) tenant already has its
real users and is left untouched. Wired through TenantDatabaseUpgradeService
and its two test configurations.

Also fixes the two pooled interest-posting services' system-user
lookup, which read username = 'system' — now wrong since that username
is personalised per tenant — to find the seeded system user by its
fixed id (2) instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-08 22:41:25 +05:30
niteshandClaude Sonnet 5 1a914a3de0 feat(fixed-deposits): maturity approval report, scoped to deposits with no closure instruction
Adds the FD maturity approval workflow: a deposit with no closure
instruction of its own (Maturity Instructions left "Not selected")
waits — status, final interest and whatever renewal is later chosen
from the FD Dashboard all run together in one real maturity run, only
once a second user approves the report, as the branch closes its day.

A deposit that already named Transfer to Savings or a Reinvest option
settles itself the moment it matures, unchanged — a decision the
customer already made in writing needs no second approval. The
nightly job is narrowed to skip only the undecided ones;
FixedDepositAccount#previewMaturityInterestPeriods reads the same
maturity-interest calculation the real posting uses, without writing
anything down, so the report's figure is never a guess.

Posting an approved report calls the existing, unchanged
DepositAccountWritePlatformService#updateMaturityDetails per deposit,
each in its own transaction — one with a bad closure target does not
hold up the others, and a retry only touches what did not post last
time.

Adds a reconciliation view (outside-approval) listing deposits that
matured via their own closure instruction, same reasoning as the
interest report's: found by what has no report line, not by naming
the instruction, so a future direct path is caught here too.

Day-close integration mirrors the interest report exactly: blocks on
a pending maturity report, posts approved ones alongside approved
interest reports, independently so one failing does not stop the
other.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-08 19:06:40 +05:30
niteshandClaude Opus 5 6e7eb94776 fix(users): encode password and email it when approving a pending user creation
finalizeCreateUser() (run when a checker approves a CREATE_USER request queued
in m_user_pending_request) saved AppUser.fromJson()'s result straight to
m_appuser via appUserRepository.saveAndFlush() with no encoding step for the
password AppUser.fromJson() auto-generates when the maker ticks "send password
to email" -- AppUser's constructor stores whatever string it is given
verbatim, so that random password was landing in m_appuser.password as plain
text. Only the maker-chosen-password case (request.getPasswordHash() != null)
was ever hashed, because that happens earlier, at submitCreateUser() time.

It also never called the email service at all, in either case. The live
create-user flow (JpaUserDomainService.create()) captures the unencoded
password, encodes it, saves, then emails the welcome message --
finalizeCreateUser() duplicates everything up to the save but stops there, so
a user created through this maker-checker flow never received their
credentials regardless of what sendPasswordToEmail said in the submitted
payload.

Fix: capture the auto-generated plaintext right after AppUser.fromJson(),
encode it with this class's own PasswordEncoder bean (already used the same
way for the maker-chosen-password branch, consistent within the file) before
saving, and call PlatformEmailService.sendToUserAccount() afterwards when
sendPasswordToEmail was true and no maker-chosen password overrides it. A
maker-chosen password is unrecoverable by approval time (submitCreateUser()
redacts it to "[PROTECTED]" immediately), so that case still cannot be
emailed, and isn't -- there is nothing wrong to fix there.

Found by manually submitting a CREATE_USER request locally and reading both
AppUser.fromJson() and the approval path end to end; no existing approved user
was re-created to verify in place, since doing so would need resetting a real
password.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-08 19:03:17 +05:30
sonali a127710442 feat(loan-products): add product master table and GL account seeding for loan interest rate registe 2026-10-08 17:29:39 +05:30
niteshandClaude Sonnet 5 6fbc3a85a8 feat(fixed-deposits): show the period each posting covers, surface maturity postings, guard against double-posting
Adds interest_period_from to the report line so each posting shows the
span it covers (e.g. "01 Oct - 31 Dec, Quarterly"), not just the date
it ends on, and a postingPeriodLabel on the report itself.

Adds a reconciliation endpoint (outside-approval) that lists fixed
deposit interest posted without this report's approval — chiefly a
deposit that matured before its own posting cycle completed, whose
last period the maturity job settles directly. Read-only, so a branch
can see every rupee of deposit interest it has paid, approved or not.

Guards posting itself against the same situation: before crediting a
line, checks whether its deposit already carries an interest posting
for that exact date (raised by the maturity job in the window between
this report being generated and being posted). If so, the line is
linked to what actually paid it instead of being paid again, and its
amount is reconciled off the report's approved total rather than
failing the whole posting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-08 00:06:14 +05:30
niteshandClaude Sonnet 5 949f45c4cd feat(fixed-deposits): FD interest posting approval with pooled GL postings
Adds a maker-checker approval report in front of fixed deposit interest
posting, mirroring the existing savings interest posting report: a
report is generated per branch and product (SINGLE/DOUBLE/SPECIAL), a
second user approves it, and it posts when the branch closes its day.
Day close is blocked until deposit interest due that day is approved.

Ledger entries are pooled per the report's interestPostingDate rather
than raised per deposit: Dr Interest Expense and the Payable/Paid legs
are each raised once for the group's total, and only the final leg
crediting each deposit's own GL is per account — the same pattern the
savings interest posting report already uses, instead of repeating the
full three-stage chain for every deposit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-08 00:06:00 +05:30
sonali 919bfb5204 Merge branch 'dev-latest' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend into feature/advanceLoanProduct 2026-10-07 15:31:57 +05:30
sonali 39e3fcec59 feat: add loan interest backend functionality
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (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-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
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (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 1 of 10) (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 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 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 Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
2026-10-07 15:29:18 +05:30
nitesh 1922455996 Merge branch 'dev-latest' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend into dev-latest
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (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 E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (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 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 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-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
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (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 - MySQL / test (test-core-1) (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 Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (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-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 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
2026-10-07 10:18:27 +05:30
keshav cca4ae583f Merge branch 'dev-latest' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend into dev-latest
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (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 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 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 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-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
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 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
2026-10-07 10:14:13 +05:30
keshav 99a7e04872 feat: updates for tds services, rbi reserve config, and cashier api resources 2026-10-07 10:14:04 +05:30
niteshandClaude Opus 5 ac7873a51e feat(shares): add preview-validate endpoint, fix document attachment 500
ClerkShareWithdrawalRequestApiResource: extracted validate(Long)'s checklist
logic into buildShareWithdrawalChecks(Map<?, ?>) and added a new
POST /preview-validate endpoint that runs it against a draft submission (same
fields submit() accepts, same Sundry-downgrade rule applied first) instead of
an already-persisted request. Lets the clerk see the exact checklist a manager
will see on approval before the request is even submitted, with zero risk of
the two checklists drifting apart, since both now call the same method. The
one check validate() runs that a draft cannot -- "Maker and Checker are
different users" -- is reported as a fixed reminder instead of silently
dropped, since the clerk previewing this is necessarily the maker and no
checker has been chosen yet.

DocumentReadPlatformServiceImpl.retrieveDocumentContent(): a document/signature
whose m_document row survives but whose underlying file is missing from the
content store (the usual cause is a database copied between environments
without its file store) was throwing an uncaught ContentStoreException,
surfacing as a raw 500 on every fetch. Now caught and converted to the same
DocumentNotFoundException (404) ImageReadPlatformServiceImpl already uses for
the identical situation on client photos, so the frontend's existing
graceful-fallback handling for a missing attachment applies here too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-06 22:40:12 +05:30
niteshandClaude Opus 5 643427bd1a fix(shares): correct share transaction lookup and validate NONE settlements
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 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 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
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Documentation build / build (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 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 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 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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (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 E2E Tests / E2E Tests (Shard 3 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 9 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-2) (push) Canceled after 0s
redemptionValueOf() read from "m_share_account_transaction" with a
"share_account_id" column. The table is m_share_account_transactions and the
column is account_id, so every share withdrawal approval failed with a
BadSqlGrammarException on the Share Sundry balance guard, before any posting was
attempted. The lookup now also filters on is_active, matching
ShareAdvanceCollectionApiResource.shareValueOf(), so a reversed purchase cannot
price a redemption.

validate() branched on "GL" versus everything else, but approve() handles a
third settlement mode: submit() downgrades a request to NONE when the clerk
names the product's own Share Sundry GL as the destination, and that mode posts
Dr Share Capital / Cr Share Sundry and stops. Validating it as a savings
settlement reported a missing fund source mapping that no posting in this mode
ever reads, so a correct request showed a failed check. NONE is now reported as
not applicable for both the linked-savings and the fund-source checks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-06 19:03:51 +05:30
niteshandClaude Sonnet 5 6c7c0f8445 build: freeze the default (non-release/maintenance branch) jar version
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 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-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
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 E2E Tests / E2E Tests (Shard 5 of 10) (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
git-versioning's rev{} fallback previously derived the SNAPSHOT version from
the nearest reachable git tag (major.(minor+1).0-SNAPSHOT), so the built jar's
name drifted as tags moved. Pins it to a fixed 1.15.0-SNAPSHOT instead so the
jar name stays predictable across builds on dev-latest/feature branches.
release/*.*.* and maintenance/*.* branches still version off their own tags,
unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-06 12:42:12 +05:30
niteshandClaude Sonnet 5 88157ee9d6 Revert "Enforce same-user maker-checker restriction"
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (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 E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 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-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-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
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
Reverts commits 2afd4aaf8 and 11a0d502b (both by anshul, same title).
Restores the CHECKER_SUPER_USER self-approval bypass in CommandSourceService
and the six *EditWritePlatformServiceJpaRepositoryImpl classes, and restores
the original isSameMakerCheckerEnabled()/isCheckerSuperUser()-gated same-user
check in PortfolioCommandSourceWritePlatformServiceImpl. Removes the
0588_persist_manually_enabled_maker_checker_permissions.xml migration added
alongside the restriction.

The unconditional same-user block these commits introduced was blocking
legitimate single-admin operation/testing (e.g. creating and then activating
a client as the same CHECKER_SUPER_USER), with no way to resolve it from
inside the app.

11a0d502b's only change (0012_seed_default_platform_api_client.xml comment
wording) is a no-op here: that exact text was independently reworded again by
82336cbe9 after 11a0d502b landed, so there is nothing left of 11a0d502b's
change to revert in that file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-06 10:02:34 +05:30
niteshandClaude Sonnet 5 760fff8d3e fix(permissions): persist manually-enabled Teller/Cashier maker-checker settings
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 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-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
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 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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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
Same pattern as 0588_persist_manually_enabled_maker_checker_permissions:
converts a manually-applied Configure Maker Checker Tasks change into a
repeatable migration, this time for the Teller/Cashier module. Found by
diffing can_maker_checker across every permission code common to
fineract_lincpay and a reference environment -- ALLOCATECASHIER_TELLER,
ALLOCATECASHTOCASHIER_TELLER, SETTLECASHFROMCASHIER_TELLER, and
UPDATECASHIERALLOCATION_TELLER were true there but still seeded false here,
confirming they were toggled on manually and never captured for any other
environment (including UAT). The _CHECKER permissions already exist and are
already granted to Clerk/Teller/Branch Manager, so approval works as soon
as this migration runs -- no role-grant changes needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-05 16:29:26 +05:30
Sonali LikhitkarandClaude Sonnet 5.5 b726577031 Update default DB password in application.properties
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 E2E Tests / E2E Tests (Shard 1 of 10) (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 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 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 Documentation build / build (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 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-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
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
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 16:15:03 +05:30
Sonali Likhitkar d1e94fd6a3 Add Fees-Penalty-Interest-Principal repayment transaction processor 2026-10-05 16:13:35 +05:30
keshav d7ff4e4a7b Merge remote-tracking branch 'origin/dev-latest' into dev-latest
Fineract E2E Tests / E2E Tests (Shard 5 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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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 E2E Tests / E2E Tests (Shard 6 of 10) (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 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-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
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
2026-10-05 12:43:38 +05:30
keshav 7068d65422 feat: teller uniqueness, cashier allocation office check and interest posting report updates 2026-10-05 12:42:44 +05:30
nitesh fd827dec27 fix(accounting,branchday): 3-stage FD interest posting in scheduler, fix branch day close business date validation
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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 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-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
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
2026-10-04 16:02:25 +05:30
nitesh aa264097e7 fix(savings,accounting): fix premature interest calculation date bounds and load interest payable GL mapping on edit
Details of changes:

1. Premature Closure 1-Day Shortfall Fix (FixedDepositAccount & RecurringDepositAccount):
- In calculatePreMatureInterest, explicitly reset transaction end-of-day balances to preMatureDate before invoking calculateInterestPayable.
- Ensure end-of-day balances are restored to current business local date in a finally block.
- Resolves the issue where premature closure missed 1 day of interest (e.g., -17.80 clawback) due to transaction end-of-day balances defaulting to the previous day when premature date matched current business date.

2. Interest Transfer Exclusions (SavingsAccountInterestPostingServiceImpl):
- Exclude interest-transfer withdrawal transactions from non-interest posting transactions and end-of-day cumulative balance updates for fixed deposits transferring interest to linked accounts.

3. Interest Payable GL Load on Edit (AccountingConstants & ProductToGLAccountMappingReadPlatformServiceImpl):
- Added INTEREST_PAYABLE(17) enum constant to CashAccountsForSavings in AccountingConstants.
- Mapped CashAccountsForSavings.INTEREST_PAYABLE to SavingProductAccountingDataParams.INTEREST_PAYABLE in ProductToGLAccountMappingReadPlatformServiceImpl.
- Fixes the bug where Liability -> Interest Payable Account GL was not loading in the product edit form (e.g., /products/fixed-deposit-products/:id/edit) even after being saved.
2026-10-04 11:54:28 +05:30
nitesh 16c67f6701 feat: Fixed Deposit - premature close logic, deposit account closure type, transaction validation, domain service and read service updates 2026-10-04 09:12:17 +05:30
nitesh e846892e9f Merge branch 'feature/bugfix' into dev-latest
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (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 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 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 Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (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-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-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-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
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
Brings in the backend counterpart of "Enforce same-user maker-checker
restriction".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

# Conflicts:
#	fineract-provider/src/main/resources/db/changelog/tenant-store/parts/0012_seed_default_platform_api_client.xml
#	fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml
2026-10-01 13:25:29 +05:30
keshav 56d98bac8d feat(rbi-reserve,savings): add Form1/Form9 returns, interest posting report and branch-day enhancements
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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 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 - PostgreSQL / test (test-core-1) (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-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
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-5) (push) Canceled after 0s
2026-10-01 13:07:58 +05:30
tarunmalviya b0e3c0ac8b DDS: vouchers, maker-checker approvals, payout & close, Sundry GL, closure tab, fixes
Fineract E2E Tests / E2E Tests (Shard 1 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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (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 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 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-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
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
2026-10-01 12:47:48 +05:30
niteshandClaude Sonnet 5 0d6066e0b6 fix(useradmin): strip password placeholder from a pending-edit replay payload
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
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 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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 E2E Tests / E2E Tests (Shard 1 of 10) (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 E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract Documentation build / build (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 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-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
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
A pending user edit's stored payload keeps a "[PROTECTED]" placeholder for
the password rather than the real value. The actual new password is applied
separately via request.getPasswordHash(), so the placeholder must not reach
AppUser.update() or it tries to re-encode the literal placeholder string.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-01 12:13:02 +05:30
niteshandClaude Sonnet 5 4499b8afad fix(accounting): post GL entries for a negative (clawback) interest transaction
A premature FD closure that reprices interest downward (e.g. a pre-closure
penalty) posts the difference as a negative interest-posting transaction --
a clawback of interest already recognised in earlier periods. Both the cash-
and accrual-based accounting processors only posted journal entries when the
transaction amount was greater than zero, so a clawback updated the FD
subledger but silently posted nothing to the General Ledger, leaving the two
out of sync.

Treats a negative amount as a mirror-image reversal: positive magnitude,
debit/credit legs swapped -- the same mechanism already used for isReversal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-01 12:12:51 +05:30
niteshandClaude Sonnet 5 11a206f42a feat(deposits): RBI premature closure compliance, simplified to always use the contracted rate
Adds a Closure Reason to premature FD closure (Normal, Premature Extension,
Conversion to Another TD, Deceased Settlement, Nominee/Legal-Heir), persisted
as an RBI audit trail on m_deposit_account_term_and_preclosure. RBI-mandated
reasons auto-waive the pre-closure penalty regardless of the maker's own
checkbox; the maker can also waive it explicitly for any other reason.

Interest rate for a premature closure is always the deposit's own contracted
rate -- the same one its regular scheduled postings use throughout its life
-- rather than a slab re-lookup against the actual period held. An earlier
"actual-period vs contracted" toggle was built, tested, then deliberately
removed per explicit request: simpler, and avoids disagreeing with interest
the account was already accruing all along.

Also fixes a real bug found while testing this: a renewed FD term's premature-
closure interest was including the previous term's closing transaction a
second time (it's dated the same day the new term starts), understating the
first day's balance by the whole prior term's payout. Scoped the interest
calculation to transactions strictly after the current term's start date.

Known open issue: a renewed/auto-renewed account can still show a materially
wrong (sometimes negative) premature-closure interest figure in some cases
(seen on account 10, a QUARTERLY-posting auto-renewed FD) -- root cause not
yet confirmed; needs further investigation with debug instrumentation before
this is considered fully correct for every renewal shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-01 12:12:40 +05:30
anshul 11a0d502be Enforce same-user maker-checker restriction
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Documentation build / build (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-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (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 E2E Tests / E2E Tests (Shard 1 of 10) (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 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 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 Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (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
2026-10-01 12:03:32 +05:30
anshul 2afd4aaf83 Enforce same-user maker-checker restriction 2026-09-30 17:07:16 +05:30
anshul e5dd6af366 Merge dev-latest into feature/bugfix 2026-09-30 16:47:41 +05:30
niteshandClaude Sonnet 5 ce8bb7e5ca fix(products): named GL account changes (e.g. Savings Interest Payable/Paid) were silently lost on a pending edit
Root cause: ProductPendingEditFieldMapper.populateCoreFields() captured
otherGlMappings with stringValueOfParameterNamed(), which only handles a
JSON string primitive. The maker's edit actually submits otherGlMappings
as a nested {assets,liabilities,expenses,income} object -- the same shape
SavingsProduct#update() already reads correctly via jsonFragment() -- so
the pending-edit row was staged with this field blank every time,
regardless of what the maker changed.

Two consequences, both now fixed:
 - The checker's diff dialog showed "No changes to review" even when a GL
   account mapping (e.g. Savings/Current/FD/RD Interest Payable or Paid)
   was genuinely changed, since the submitted value never made it into
   proposedChanges.
 - Approving the edit replayed a payload with no otherGlMappings at all,
   so the GL account change was silently dropped rather than applied --
   the edit showed as approved, but the product's accounting mapping
   never actually moved.

Fixed the write side (jsonFragment instead of stringValueOfParameterNamed)
and the read-side replay (writeCoreFieldsToJson now re-adds the stored
value as a parsed JSON element via the new putJsonFragment, not a quoted
string, which would have double-encoded it on replay). Shared by Savings,
Current, FD and RD products, since they all go through this one mapper.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-30 12:39:58 +05:30
niteshandClaude Sonnet 5 82336cbe98 fix: remove illegal -- from migration 0012's XML comment
Same mistake as an earlier migration this session: a literal "--" inside
an XML comment is illegal per the XML spec and crashes the entire backend
startup (Liquibase's XML parser rejects the whole changelog, not just this
file). Reworded the prose to avoid it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-30 12:00:59 +05:30
Sonali Likhitkar 22c4403a14 feat(loan-product): add advance loan product ext entity, API and liquibase 2026-09-29 17:56:50 +05:30
niteshandClaude Sonnet 5 9436eab129 feat(security): seed a fixed default platform API key via migration instead of a random one at runtime
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (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-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
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 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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 E2E Tests / E2E Tests (Shard 1 of 10) (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 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 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 Documentation build / build (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
Replaces the previous DefaultApiClientBootstrapper (ApplicationRunner
minting a random key on first startup) with a plain Liquibase seed on the
tenant-store database: one fixed, well-known api_key_hash row, inserted
once via migration 0012, same on every deployment and every tenant --
exactly like this fork already seeds default codes/permissions, rather
than generating a secret at runtime.

The trade-off is explicit in the migration's own comment: a fixed key
checked into source is a bootstrap convenience, not a secret, and any
deployment beyond local/dev must rotate it (insert a new hash, deactivate
this row) before going live -- rotation is a direct m_api_client write,
same as the seed itself, since POST /v1/apiclients stays disabled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:01:07 +05:30
niteshandClaude Sonnet 5 1408bddf58 feat(security): auto-mint the first platform-operator API key on deploy, disable minting one over HTTP
A fresh deployment's m_api_client table starts empty, and reaching
#/platform-admin/menu-access requires a valid X-Api-Key -- with no key,
every tenant 401s identically (this is by design, not tenant-specific).

Add DefaultApiClientBootstrapper: an ApplicationRunner that mints exactly
one random key on first startup (guarded by "table still empty", so later
restarts are a no-op) and logs the raw value once to the server's own
application log -- never to the database, never over HTTP.

Disable ApiClientAdminApiResource's POST /v1/apiclients entirely: it only
required ALL_FUNCTIONS, a per-tenant role grant, so any bank's own super
user (or a misconfigured role, as "Clerk" was found to carry it in two
tenants this session) could mint themselves a platform-wide operator key
and bypass the exact ceiling TenantMenuAccessApiResource exists to enforce.
New keys past the first are now a direct m_api_client insert only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 15:36:18 +05:30
anshul e02ab51ee1 Merge branch 'dev-latest' into feature/bugfix
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (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 4 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 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 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 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 Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 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 - PostgreSQL / test (test-core-1) (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
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
2026-09-29 10:21:21 +05:30
nitesh 3a37a5f90f Merge branch 'feature/bugfix' into dev-latest
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (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 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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (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-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
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
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

# Conflicts:
#	fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml
2026-09-28 18:36:12 +05:30
niteshandClaude Sonnet 5 e7417d16a6 chore: renumber migration 0585 to 0587 ahead of feature/bugfix merge
feature/bugfix independently used 0585/0586 for unrelated migrations
(loan COB catch-up, system module checker permissions), colliding with
this session's own 0585 file. Renumbered to the first free slot after
theirs so both sets of changesets coexist without ambiguity; content and
semantics are unchanged, only the file name and changeset ids moved.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 18:35:11 +05:30
niteshandClaude Sonnet 5 24d3bf83bb fix: correct Current-product OD columns, PortfolioProductType mapping, and duplicate Bank Transfer voucher
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (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 Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (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 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 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-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-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
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
- Add migration 0585 to add the OD-scheme columns to the correctly-named
  pending-edit shadow table (m_saving_product_temp, singular), fixing
  migration 2033's typo that made it a permanent no-op and broke editing
  any Current product with OD fields set.
- Fix PortfolioProductType.fromInt() to switch on each constant's declared
  value instead of declaration position, which silently mis-mapped ids 3-5
  and NPE'd on id 6 (CURRENT), crashing Journal Entries for Current-account
  GL rows.
- Stop generating a second Bank Transfer voucher at approve time; reuse the
  one already created at raise/submit time instead, eliminating duplicate
  vouchers for the same transfer under two different users' names.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 17:53:25 +05:30
anshul af052dcb4f Merge branch 'dev-latest' into feature/bugfix
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 1 of 10) (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 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 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-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 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 - MariaDB / test (test-core-2) (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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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
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
2026-09-28 17:22:25 +05:30
keshav b12268e740 feat: add RBI Form A and Form 9 SLR filing APIs, jobs, and migrations
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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 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-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
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
2026-09-28 13:35:32 +05:30
anshul 6ee3bbe67c feat: add command handlers, permissions, and API resources for surveys and loan COB catch-up
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 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 1 of 10) (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 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 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 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 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-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
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
2026-09-28 12:55:21 +05:30
anshul d8f6c25875 Merge branch 'dev-latest' into feature/bugfix
especially if it merges an updated upstream into a topic branch.
2026-09-28 12:39:03 +05:30
niteshandClaude Sonnet 5 2155c94161 fix(fd): Reinvest Principal Only renews in place, same account, instead of opening a new one
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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 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-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
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
Reinvest Principal Only was still on the legacy close-and-reopen path (open
a new FD with just the principal, close the old one) while Reinvest
Principal + Interest and Partial Reinvest had already moved to renewInPlace
-- the same customer choosing a different split ended up with two different
outcomes: same account number, or a brand new one. Unified all three onto
renewInPlace: Principal Only is now exactly Partial Reinvest with the split
fixed at the closing principal, so it renews on the same account with only
the leftover interest actually leaving (to the chosen/linked Savings
account), in both handleFDAccountPayment (Payment dialog / FD Dashboard
Renew) and handleFDAccountMaturityClosure (the nightly job's path for an
account with an already-stored instruction).

Also fixes a related bug found while testing this: recordInPlaceRenewal
re-archived the closing term's full rate chart on every renewal of the same
term, even when it had already been archived by an earlier renewal --
archiveChartSlabs now only runs the first time a term's closing row is
created.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 11:41:18 +05:30
niteshandClaude Sonnet 5 87f3e591ce fix(accounting): tag current-account GL entries entity_type_enum=CURRENT instead of reusing SAVING, everywhere this codebase links an entry to its account
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 Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (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 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 Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (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 1 of 10) (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 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 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 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-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
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
m_current_account and m_savings_account each keep their own identity
sequence, so the same numeric id can be a real row in both. Every place
that links a GL journal entry back to its account for display -- so the
Journal Entries screen shows an Account No instead of a blank "-" --
was tagging entity_type_enum=2 (PortfolioProductType.SAVING)
unconditionally, even when the account was actually a current account.
Whenever a current account's id collided with an unrelated savings
account's id, the entry resolved to the wrong one entirely: a customer's
savings deposit could display someone else's current account number, or
vice versa.

Fixed at the write side, in every place found doing this:
- ClerkDisbursementRequestApiResource (Bank Transfer, Collection,
  Disbursement) -- GlEntry now carries an isCurrentAccount flag, decided
  once where the account was already resolved, instead of being
  re-guessed.
- ChequeBookIssuanceChargeService, ClearingJournalService,
  SponsorDDIssueRequestWritePlatformServiceImpl -- same pattern, each
  already knew savings vs current at the point it linked the entry; that
  answer is now threaded through instead of discarded.

Also fixed on the read side:
- JournalEntryReadPlatformServiceImpl: entity_type_enum=6 now resolves
  unambiguously to the current account; entity_type_enum=2 still falls
  back to savings_transaction_id (a reliable signal only a genuine
  savings transaction sets) before guessing, for older rows written
  before this fix.
- JournalEntryReferralServiceImpl (AML referral resolution): the same
  ambiguity could attribute a suspicious-activity alert to the wrong
  client entirely -- a same-numbered stranger's account -- when a
  current account's entity_id collided with a savings account's.
  Resolved with the same entity_type=6 / savings_transaction_id
  precedence as the read-side fix.

Not touched: JournalEntryAggregationJobReader (year-end aggregation
batch job) has the same ambiguity for legacy entity_type_enum=2 rows,
but only ever joins m_savings_account -- it has no current-account
handling at all, standard-Fineract in origin. Extending it is a real
feature addition, not a targeted fix, and touches a scheduled financial
batch job this session cannot rebuild or run to verify; flagged for
separate, deliberate work instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 10:34:39 +05:30
niteshandClaude Sonnet 5 1e903232f0 fix(fd): pay late-renewal gap interest to the linked Savings account, not the FD
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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-5) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (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 - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (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-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
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (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 1 of 10) (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 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 E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Gap-period interest is earned at the linked account's rate, not the FD's --
folding it onto the FD's own balance meant "Re-Invest Maturity Amount"
silently carried savings interest into the new FD term as if it were FD
interest, and "Re-Invest Principal Only" paid it out through the FD's own
closure math instead of it already being where it belongs. Now posted
directly onto the linked account as its own INTEREST_POSTING transaction;
the FD's balance is left untouched, so every closure type renews/transfers
its real maturity amount. Renewal Preview updated to match (projectedBalance
is now just the FD's own balance) and now also returns the linked account's
own account number, so staff can see exactly where the gap interest is
about to land before confirming.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 14:11:50 +05:30
niteshandClaude Sonnet 5 2c49d6f637 feat(fd): add read-only renewal preview endpoint, fix stale linked-account reads
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 - MySQL / test (test-core-1) (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-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
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (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 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 E2E Tests / E2E Tests (Shard 5 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 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 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
GET /fixeddepositaccounts/{id}/renewal-preview -- shown in the FD
Dashboard's Payment dialog before staff confirm a late renewal, so they see
the actual numbers (linked account's rate, gap days, gap-interest amount,
projected new balance) instead of confirming blind. Shares its calculation
with the posting path (creditGapInterest) via a new computeGapInterestAmount
helper, so the previewed number is exactly what gets credited.

Also fixes fdAccount.getLinkedAccountId() returning null for an account
confirmed to have linked_account_id set in the database -- every caller
that needs the FD's Deposit Account Link now reads it fresh via SQL
(resolveLinkedAccountId) instead of that stale JPA entity accessor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 12:40:55 +05:30
niteshandClaude Sonnet 5 331c553164 fix(accounting): interest postings were never reaching the general ledger
createInterestPostingJournalEntriesForSavings() documents a 3-stage
posting (Dr Interest On Savings -> Cr Payable, Dr Payable -> Cr Paid, Dr
Paid -> Cr Savings Control), but the first stage was never actually coded
-- the interest expense account was never even fetched. For a product with
no distinct Interest Payable/Paid accounts configured (every product in
this system, checked directly), the remaining two stages then collapsed
into a same-account no-op that correctly refuses to post, leaving interest
silently added to the account's balance with zero GL entries. Affects every
deposit type equally (Savings, FD, RD), not just FD -- confirmed by
checking every product's accounting mappings. Added the missing stage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 12:40:46 +05:30
niteshandClaude Sonnet 5 3fb008a22a fix(products): clearer error message for an FD product amount field that's too large
The FD product's exception mapper fell back to a bare "Unknown data
integrity issue with savings product." for any DB error it didn't
recognize, with no detail -- unlike its sibling class for regular Savings
products, which at least appends the underlying error. Added a specific,
actionable message for the numeric-overflow case (an amount field entered
too large for its column), and made the generic fallback include the real
detail too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 12:40:28 +05:30
niteshandClaude Sonnet 5 8b2fb052d4 fix(fd): keep a genuinely-matured FD on the Dashboard instead of dropping it
The maturity-dashboard query only ever selected ACTIVE(300) accounts. That
was fine while nothing ever really reached MATURED(800) in this dev setup
(test dates were only SQL-backdated, never run through the real nightly
job), but the first time the job actually flipped an account's real status,
it silently vanished from the ladder instead of showing under "Matured" --
exactly the state the ladder exists to surface. Now selects ACTIVE or
MATURED.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 12:40:21 +05:30
anshul cebba87864 Merge branch 'dev-latest' into feature/bugfix
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-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
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (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 E2E Tests / E2E Tests (Shard 1 of 10) (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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 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 Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Documentation build / build (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
2026-09-25 11:24:24 +05:30
niteshandClaude Sonnet 5 ea39c8e82a fix(current-account): recognize Withdrawn and Rejected as terminal when checking for a duplicate product application
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (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 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 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 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-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
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 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
countActiveByClientAndProduct only excluded status 600 (CLOSED), so a
client whose prior application for the same product was Withdrawn (400)
or Rejected (500) -- both terminal, per CurrentAccount.withdraw()/
reject() -- was still blocked from applying again, same as if that
account were genuinely still open. Only 100/200/300 (submitted,
approved, active) should collide with a fresh application.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 11:22:31 +05:30
niteshandClaude Sonnet 5 32411c924b fix(accounting): stop the Journal Entry view showing a colliding current account's number for a savings GL entry
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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 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 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-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
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (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 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
entity_type_enum=2 (PortfolioProductType.SAVING) is written for both
savings and current-account journal entries -- current products
deliberately share SAVING's GL type space -- so it can't distinguish
them, and m_savings_account/m_current_account each have their own id
sequence, meaning an entity_id can be a real row in both. The account-
number column's COALESCE(ca.account_no, sa.account_no) always preferred
the current account's number whenever both matched, so a savings
deposit's own journal entry displayed an unrelated current account's
number instead of the account it actually posted against.

savings_transaction_id is the one reliable signal available on this
query: only a genuine savings transaction sets it. Checking it first
resolves the correct account whenever it's present, and falls back to
the previous behavior only when it's absent (current-account entries,
which don't set it).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 10:58:44 +05:30
niteshandClaude Sonnet 5 f696499102 fix(current-account): stop account-id collisions between savings and current accounts from misrouting money and losing GL entries
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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 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-3) (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-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
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 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 - MySQL / test (test-core-1) (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-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
m_current_account and m_savings_account each keep their own identity
sequence, so the same numeric id can be a real row in both -- an id-only
lookup used to decide "is this a current account?" once sent a real
deposit into an unrelated client's current account instead of the
savings account a request actually named, across every entry point that
did this: cashier deposit approval/reprocess, clerk withdrawal approval,
and clerk disbursement collection/disbursement/bank-transfer. All of
those now also verify the account_no before trusting an id match.

Separately, GL posting for current-account transactions has been
silently failing since acc_product_mapping had two rows for the same
(product_id, product_type, financial_account_type) slot -- current
products share PortfolioProductType.SAVING's id space with real savings
products, and a NULL payment_type doesn't collide under that column's
own unique constraint, so duplicates went unnoticed. The single-result
repository lookup that read this mapping threw on the duplicate, which
was swallowed by a try/catch, so account balances kept updating while no
journal entry was ever posted. Both current-account GL lookups now take
the most recently created mapping instead of failing outright.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 18:14:32 +05:30
anshul 7eabaf2542 Merge branch 'dev-latest' into feature/bugfix
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 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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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 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-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
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
2026-09-24 18:09:48 +05:30
keshav 4b9093007f feat: update TDS config, RBI reserve, current products, OSS returns, and dormancy services
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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 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-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
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
2026-09-24 18:06:29 +05:30
anshul 9beae3df28 Implement organization maker-checker workflows
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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 Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (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-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
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
2026-09-24 17:22:39 +05:30
niteshandClaude Sonnet 5 d2e13cc7b9 feat(fd): dashboard-driven maturity renewal, plus fix stranded interest on Reinvest Principal Only
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Documentation build / build (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 E2E Tests / E2E Tests (Shard 1 of 10) (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 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 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 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 - 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 - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (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-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
A SINGLE/DOUBLE FD opened with an explicit maturity instruction still auto-processes on its own
via the nightly job, exactly as before -- the grace period never delays that. An FD opened with
NO instruction at all (the new default -- see the frontend's account-creation changes) is now
never touched by the job; it just sits matured until staff renew it manually from the FD
Dashboard, backdated to the original maturity date within the product's grace window, or from
today with gap-period interest (at the linked account's rate) credited first once it's passed.

New endpoints:
- POST /fixeddepositaccounts/{id}/renew -- the Dashboard's own quick Reinvest choice
  (Principal+Interest / Principal Only), for the two-option case.
- POST /fixeddepositaccounts/{id}/credit-gap-interest -- a pre-step the frontend's Payment
  dialog now calls before submitting the existing generic `payment` command, so the Dashboard's
  Renew action offers every closure type Payment does (Transfer to Savings, both Reinvest
  options, Partial Reinvest), not just two, while still crediting gap-period interest first.

Also fixes a real, pre-existing bug found while building this: REINVEST_PRINCIPAL_ONLY never
actually paid its leftover interest anywhere -- handleFDAccountMaturityClosure withdrew only the
principal into the new deposit and never referenced its own toSavingsId parameter, silently
stranding the interest on an account it also never closed. Now transfers the remainder to the
target savings account and actually closes the old account.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 15:55:25 +05:30
niteshandClaude Sonnet 5 ecfe8aa76a feat(fd): add Renewal Grace Period product field, both storage paths
New per-FD-product "Renewal Grace Period (Days)" term field: null/0 (every existing product)
keeps today's instant, same-night auto-renewal; a positive value is later read by the nightly
maturity job and the manual Renew action to decide whether/how a matured FD waits for staff to
act instead of renewing itself.

Wired into the live product path end-to-end -- DepositsApiConstants param, DepositTermDetail
column + diffing, DepositProductAssembler (create and update), DepositProductReadPlatformServiceImpl,
FixedDepositProductData -- migration 0572 adds the column.

Also wired into the maker-checker pending-edit mirror (m_saving_product_temp /
FixedDepositEditFields / ProductPendingEditFieldMapper), a second, independent storage path a
governed product edit uses -- without this the field silently dropped out of a checker's
"Proposed changes" review even though it was submitted. Migration 0573 adds its column.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 15:55:11 +05:30
niteshandClaude Sonnet 5 a7cb09f15c feat(fd): add lean FD maturity dashboard read endpoint
New GET /v1/fixeddepositaccounts/maturity-dashboard: a narrow, purpose-built query for the FD
Dashboard's maturity ladder (account/client/product identity, maturity date, deposit/maturity
amounts) instead of routing through the general FixedDepositAccountsApiResource#retrieveAll,
whose mapper joins six extra tables for audit/currency/tax fields this screen never shows.

Adds the index migration (0571) the dashboard's WHERE clause needs -- neither
m_savings_account.deposit_type_enum/status_enum nor
m_deposit_account_term_and_preclosure.maturity_date had one before, so the endpoint would
otherwise run a full table scan on top of being wide.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 15:54:57 +05:30
niteshandClaude Sonnet 5 26d4c241c7 fix(clerk-disbursement): validate savings withdrawal eligibility when a request is raised
A clerk raising a disbursement request with a Dr leg against a savings account had no way to
know the withdrawal would be refused (inactive account, still locked-in, or insufficient
balance against its own minimum) until a manager tried to approve it and the whole request
bounced back. Checks the same conditions SavingsAccount.withdraw() enforces at entry time
instead, against the same columns, so the clerk sees the real reason immediately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 15:54:03 +05:30
niteshandClaude Sonnet 5 dbc2737ada fix(deposits): FD interest posting silently missed its GL entry on single-period maturity
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 E2E Tests / E2E Tests (Shard 1 of 10) (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 E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
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 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 Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (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
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
SYMPTOM
An FD account whose entire term fits inside one interest-posting
period (e.g. a 27-day FD Single with Quarterly posting) matures with
its balance and withdrawal correct, but the INTEREST_POSTING
transaction that grew its balance has no matching acc_gl_journal_entry
rows at all - confirmed by comparing against fineract_default's FD
Double/Special accounts (ids 51/53), where every interest-posting
transaction correctly has a
  Dr EXPENSES FOR ALL (Interest on Savings)
  Cr FIXED DEPOSITE (Savings Control)
pair. Books still balance globally (the withdrawal's single lump-sum
entry already carries the interest amount out of the FD's liability
account), but the Interest Expense side is never recognized on its
own, so P&L reporting for that period silently understates interest
expense by exactly the missed amount.

ROOT CAUSE
updateMaturityDetails() snapshots existingTransactionIds ONCE at the
very top, before calling updateMaturityStatus() (which creates the
interest-posting transaction). The old comment here said this was
deliberate - "interest postings ... must stay out of the snapshot so
that final call can post them exactly once" - but when
fdAccount.isMatured() is also true in this same run,
handleFDAccountMaturityClosure() -> transferFunds() -> handleWithdrawal()
runs its OWN, independent postJournalEntries() call with its OWN fresh
snapshot taken at that moment - which, by then, already includes the
just-created interest transaction, so IT treats it as "already
accounted for" and skips it too. Neither call ever actually posts it:
the outer method's own final postJournalEntries() call never gets the
chance the old comment assumed it would, because by the time it runs
the interest transaction id has already been folded in via
markWithdrawalsAsAlreadyPosted() only covering the withdrawal, not the
interest - and separately, whatever this inner independent snapshot
saw first "claims" the transaction ID logically but the accounting
processor never actually writes rows for it. Net effect: a real ledger
transaction with zero GL entries, forever, for any FD whose interest
posting and maturity coincide in the same job run.

Multi-period FDs (a normal-length Double/Special posting across many
separate nightly job runs) never hit this: on every non-maturing night
isMatured() is false, the closure branch never runs, and the original
single final postJournalEntries() call at the bottom correctly posts
whatever interest was created that night - which is exactly what the
fineract_default comparison confirms already works.

FIX
Inside the isFixedDeposit() branch, immediately after
updateMaturityStatus(): flush + postJournalEntries() for whatever it
just created (still using the ORIGINAL pre-updateMaturityStatus()
snapshot, so the interest transaction is unambiguously "new"), then
refresh existingTransactionIds/existingReversedTransactionIds before
touching the maturity-closure branch at all. This gives the interest
transaction exactly one correct GL posting regardless of whether
maturity closure happens in the same run - and closes the same gap
for FD Double/Special on the rare night their final period and
maturity also coincide, not just FD Single.

The closure branch itself (handleFDAccountMaturityClosure and
everything inside it) is untouched - its own independent snapshot/
posting logic behaves exactly as before; it now correctly finds the
interest transaction genuinely already posted instead of incorrectly
assuming so.

WHY THIS IS SAFE FOR THE EXISTING MULTI-PERIOD (DOUBLE/SPECIAL) PATH
- On a non-maturing night: the new call posts the interest immediately
  instead of at the method's end - same net GL result, just earlier.
  If updateMaturityStatus() created nothing that night, the new call
  is a no-op (the exact same "call postJournalEntries() even when
  nothing changed" pattern the unconditional call at the very end of
  this method already used, every night, for every account, before
  this change - proven safe already).
- The RecurringDepositAccount (else-if) branch is completely untouched.

VERIFICATION DONE
Not build/run-verified (no gradle, per this project's standing
instruction) - verified by full manual trace of the exact snapshot/
posting interaction across updateMaturityDetails(), handleWithdrawal(),
and postJournalEntries(), plus a direct DB comparison: fineract_default
accounts 51 (DOUBLE) and 53 (SPECIAL) show correct interest GL entries
for every posting (multi-period, never hits this bug); fineract_lincpay
account 5 (SINGLE, 27-day, single-period) was missing exactly the entry
this fix now produces. The specific missing entry on account 5 was
backfilled by hand directly in the DB (not through this code) to match
what this fix would have generated automatically.

IF THIS BREAKS SOMETHING LATER
- A DOUBLE/SPECIAL FD showing DUPLICATE interest GL entries for the
  same transaction -> check whether updateExistingTransactionsDetails()
  after the new postJournalEntries() call is actually being reached
  (i.e. no exception thrown by that call swallowing the refresh).
- An FD's interest-posting transaction still missing its GL entry ->
  query acc_gl_journal_entry WHERE savings_transaction_id = <the
  INTEREST_POSTING txn id> and confirm 0 rows; if so, this fix (or its
  ordering relative to a future change here) has regressed.
- Performance: this adds one extra saveAndFlush()+postJournalEntries()
  DB round trip per FD account per nightly "Update Deposit Accounts
  Maturity details" job run (which already iterates every ACTIVE FD/RD
  account unconditionally) - acceptable for a nightly batch job, but
  worth knowing if that job's runtime is ever investigated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 14:21:40 +05:30
niteshandClaude Sonnet 5 f457b9b8c7 fix(cashier): route FD/RD cashier deposits through maker-checker approval
SYMPTOM
A cashier could credit an FD/RD account with cash and the money moved
in instantly, with no Manager approval step - unlike a Savings/Current
deposit from the same screen, which correctly creates a PENDING
m_cashier_deposit_request. Discovered by tracing why a real FD
funding deposit never showed up in Branch Ops -> Approvals: it had
posted directly, no request record ever existed for it.

ROOT CAUSE (two layers, both fixed here / in the paired frontend commit)
1. Frontend gate (transaction-entry.component.ts confirmSubmit(), see
   frontend commit): the maker-checker branch for a cashier deposit
   only matched row.acTyp in ('SD','CA','CB','GL') - 'FD' and 'RD' were
   never in that list, so a deposit into either fell through every
   other branch to the generic, unguarded direct POST at the bottom of
   confirmSubmit().
2. Even with the frontend gated correctly, approve() here only knew
   how to tell Current apart from "everything else -> plain savings
   deposit command" (isCurrentAccountById()). FD/RD accounts share the
   m_savings_account table with plain Savings (distinguished only by
   deposit_type_enum 200/300 vs 100), so an FD/RD deposit request would
   have been posted through the generic savingsAccountDeposit command,
   which does not accept a term-deposit account id.

FIX
- resolveDepositAccountCategory(accountId): looks up the real category
  fresh from the DB (m_current_account / m_savings_account.
  deposit_type_enum) instead of trusting the request's own account_type
  string - same defensive pattern this class already used for Current.
- New executeFixedDepositDeposit() / executeRecurringDepositDeposit():
  post through Fineract's actual FD/RD deposit commands
  (CommandWrapperBuilder#fixedDepositAccountDeposit /
  #recurringAccountDeposit, hitting /fixeddepositaccounts/{id}/
  transactions and /recurringdepositaccounts/{id}/transactions) instead
  of the generic savings command.
- approve() now branches on all four categories (Current / Fixed
  Deposit / Recurring Deposit / Savings) instead of two.

RELATED DATA CLEANUP DONE THIS SESSION (not in this commit)
The one FD deposit made before this fix existed left an orphaned
m_savings_txn_denomination row (physically "counted" cash with no
approved request backing it), which inflated the Branch Day Register's
"Vault + Cashiers" total above the approved-requests-only "book" total
by exactly that amount every day going forward. That artifact row was
deleted directly (not backfilled with a fabricated approval - the
Claude Code safety layer correctly refused that approach as audit-log
tampering). No code in this repo does that cleanup automatically.

IF THIS BREAKS SOMETHING LATER
- A cashier FD/RD deposit posting instantly again -> check the
  frontend gate list in confirmSubmit() (paired commit) still includes
  'FD'/'RD'.
- approve() throwing on an FD/RD deposit request -> check
  resolveDepositAccountCategory() is returning FIXED_DEPOSIT/
  RECURRING_DEPOSIT (query m_savings_account.deposit_type_enum for the
  account id in question: 200 = FD, 300 = RD).
- Branch Day Register showing a mysterious Vault-vs-book Excess again
  -> check for an m_savings_txn_denomination row with no matching
  m_cashier_deposit_request; that combination should be impossible now
  that this fix is in place, so a new one appearing means this fix (or
  its frontend half) regressed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 14:21:13 +05:30
niteshandClaude Sonnet 5 23ee4f1063 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 <noreply@anthropic.com>
2026-09-23 14:20:55 +05:30
niteshandClaude Sonnet 5 bc38216ca3 fix(deposits): FD Single Rate Calculator crashed for tenures under 30 days
SYMPTOM
Opening the FD account create screen for a "SINGLE" (Days < 1 Year)
product, entering a Deposit Tenure under 30 days (e.g. 27), and
clicking "Find Rate" returned a 400:
  "The parameter `tenureInMonths` must be greater than 0."

ROOT CAUSE
The Rate Calculator preview always converted the entered tenure to
months before calling GET /fixeddepositaccounts/calculate-fd-interest
(fixed-deposits.service.ts -> fixed-deposit-account-interest-rate-
chart-step.component.ts, calculateRate()):
  tenureInMonths = Math.floor(enteredDays / 30)
For any tenure under 30 days this floors to 0, and
DepositAccountDataValidator.validateForInterestCalc() requires
tenureInMonths > 0 unconditionally, so the request was rejected
before it ever reached the calculation service.

FIX
Added `tenureInDays` as an alternative to `tenureInMonths` for this
one calculator endpoint only (DepositsApiConstants.
FIXED_DEPOSIT_ACCOUNT_INTEREST_CALCULATION_PARAMETERS) - not a
general-purpose FD field, not touched anywhere else:
  - DepositAccountDataValidator: tenureInMonths is required unless
    tenureInDays is a positive value (either satisfies the check).
  - FixedDepositAccountsApiResource#calculateFixedDepositInterest:
    accepts the optional tenureInDays query param.
  - FixedDepositAccountInterestCalculationServiceImpl#calculateInterest:
    when tenureInDays > 0, the exact-calendar maturityDate is computed
    via submittedOnDate.plusDays(tenureInDays) instead of
    plusMonths(tenureInMonths) (which would silently compute a 0-day
    maturity window for a sub-month tenure).
  - Frontend (separate commit) now sends tenureInDays instead of a
    truncated-to-zero tenureInMonths whenever the entered tenure is
    under 30 days.

SCOPE / WHAT THIS DOES NOT TOUCH
Only the interest-preview calculator endpoint above. Real FD/RD
account submission still uses depositPeriod + depositPeriodFrequencyId
(Days=0) exactly as before; this commit does not change how an actual
account's maturity date/amount gets computed at activation.

IF THIS BREAKS SOMETHING LATER
- "tenureInMonths must be greater than 0" reappearing for a SHORT FD
  tenure -> check the frontend is actually sending tenureInDays (network
  tab on the Find Rate call); the validator's OR-logic is in
  DepositAccountDataValidator.validateForInterestCalc().
- A wrong preview maturity amount for a sub-month FD Single -> check
  FixedDepositAccountInterestCalculationServiceImpl#calculateInterest's
  branch choosing plusDays vs plusMonths for maturityDate.
- This bug and fix were previously implemented once, then fully
  reverted mid-session at the user's request (unrelated Deposit Account
  Link regression got tangled up with it), then reapplied cleanly here
  as a standalone, narrowly-scoped fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 14:20:38 +05:30
tarunmalviya fa8e5f3f16 Merge remote-tracking branch 'origin/dev-latest' into feature/dds-agent-settlement-reports
# Conflicts:
#	fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
#	fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java
#	fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
2026-09-23 10:23:37 +05:30
tarunmalviyaandClaude Opus 5 12fdf035ca DDS: collection entries held before posting, per-agent GL accounts, maturity auto close
Collections used to reach a customer the moment they were typed, on the reasoning that the money is
theirs from the moment the agent takes it. That holds until the agent does not arrive - and then the
books say a customer was credited with notes the bank never held, and there was nothing to count the
agent's bag against when they did.

An agent now records their own round. Those entries sit in m_dds_collection_entry and move nothing:
no balance, no ledger, no agent cash. A clerk counts the cash against what they add up to and
settles the day, and what that writes is an ordinary collection - posted through the counter's own
code rather than a copy of it, so the two cannot drift apart.

Each agent may also hold their own ledger account now, one agent to an account. Both sides of the
entry, their direction and the amount are unchanged; only which account plays the agent's part
differs, and only for agents that name one. What is left standing on an agent's own account is the
cash they are holding and have not brought in - a question nobody could answer while every agent
shared a single account.

A scheme can close its own accounts on maturity, for accounts whose customer named where the
proceeds go. The run is generated and reviewed before anything moves, and an account that named no
destination is simply not on the list and is paid out by hand as before.

Also: the payment mode is a fixed set and stored one way; an agent's birth date must be in the past
and of age; a maturity credit account is checked whenever it is written rather than only when the
Adv Code happened to arrive beside it; and two report columns stop printing money ungrouped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 22:16:14 +05:30
keshav fde2699957 feat: sponsor bank DD payable GL, AML stale review release, and dividend updates
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 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 E2E Tests / E2E Tests (Shard 1 of 10) (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 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 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 9 of 10) (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 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-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
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 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 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
2026-09-21 10:23:52 +05:30
niteshandClaude Opus 5 dd4bb4a1c0 feat(shares, cashier): enforce Share Collection entry type on listing, single receipt fetch, product summary
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 10 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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (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 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 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 Messaging Smoke Tests / Smoke Test with Kafka (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-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
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
Only vouchers tagged with the "Share Collection" entry type reach a share
account. The adoption path already checked this, but the listing did not, so an
already-adopted "General" voucher kept reappearing at Apply Collection and Apply
Additional Shares time. SHARE_COLLECTION_TAGGED_ONLY now filters the listing
query itself, against both the cashier-deposit and clerk-disbursement request
tables.

/unattributed takes an optional clientId and answers with both lists in one
response -- that member's own unapplied receipts under "own", every other
available receipt under "rows", with the member's own excluded from the latter.
The two endpoints read the same table under the same filters, so asking twice
only ever fetched the same rows twice, and keeping the query in two places is
what let the entry-type filter land on one and miss the other. Both now share
unappliedFor().

The entry fee is never posted a second time. A receipt already sitting in an
INCOME GL was recognised as income by the cashier's own entry when it was taken,
so applying the collection now skips both legs for it and counts it only towards
verifying the fee was collected.

Adds GET /sharereports/product-summary: authorised, issued, available and unpaid
shares per product, with the distinctive range and account count.

Adds a series-overlap pre-check to sponsor DD inventory registration, which
previously surfaced as a bare duplicate-key error.

Adds migration 0568 for the columns the clerk share-withdrawal settlement path
reads (settlement_mode, tok_no, cash_settlement_txn_id, cashier_id, disbursed_by,
disbursed_at, disbursement_denomination_data), each changeset guarded so it is a
no-op where the column already exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 18:58:40 +05:30
niteshandClaude Sonnet 5 6046134b51 fix(shares): drop stale client-mismatch check, fix stale client_id on settle
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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-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 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-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
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
- Remove the client-mismatch validation in the Additional Shares tranche
  submit path (ShareAccountDataSerializer) for parity with the same decision
  already made on the first-application Apply Collection flow -- a voucher a
  counter was shown and allowed to pick should not then be rejected for the
  same reason at the very last step
- Rewrite m_share_advance_collection.client_id to the settling account's own
  client at the moment a voucher is actually applied (both
  ShareAdvanceCollectionApiResource.apply() and
  AdditionalShareTrancheFundingService.settle()) -- a voucher can now be found
  and applied from any member's screen, so whoever last searched it is not
  necessarily who its money became share capital for, and the stale value was
  misnaming the receipt in its own detail popup after settlement

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 13:56:49 +05:30
nitesh e4fad8b648 Merge branch 'dev-latest' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend into dev-latest
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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 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-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
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
2026-09-18 13:11:10 +05:30
niteshandClaude Sonnet 5 e206536a6b feat(shares): reports suite, GL transaction fixes, strict entry-type enforcement
- Add ShareReportsApiResource (final balance, account statement, issued/status/
  nominal/share-wise/guarantor/member-interest reports, enquiry, payment
  certificate, share detail, dividend statement, folio statement, distinctive
  numbers, area-wise, mailing list) and ShareTransferApiResource, applying the
  paid-up-capital predicate everywhere so an unsettled application never
  counts as issued capital
- Add share transfer migration (0567)
- Enforce the original application's collection before a further tranche can
  be applied (ShareAccountDataSerializer), matching the same gate on screen
- Fix postGlOffsetEntry silently discarding the real GL transaction id on the
  clerk-collection fallback posting path, leaving gl_transaction_id blank and
  the receipt's GL-detail popup empty
- Add gl_code to the by-voucher and adoption responses so the GL Account
  picker on screen follows whichever receipt was found or adopted
- Require an exact "Share Collection" entry-type tag to adopt a voucher --
  "General" and untagged rows no longer qualify

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 13:02:55 +05:30
niteshandClaude Sonnet 5 bf52c2cda1 feat(shares): year-safe voucher numbers, universal Share Collection visibility, unrestricted apply
Voucher numbers now embed the year (PREFIX/DDMMYYYY/NNNN) so the same day
and month a year apart can never produce the same voucher_no text, which
every lookup/dedup across the cash registers matches on literally.

/shareadvancecollection/unattributed now lists every unapplied Share
Collection receipt across every share account screen, not just ownerless
GL deposits -- filtered to rows whose current Entry Type is still Share
Collection even after adoption, with the linked member named for
transparency. Applying a receipt to a share account no longer blocks on a
client mismatch, per product decision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 12:48:22 +05:30
keshav 934c11bf87 Merge branch 'dev-latest' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend into dev-latest
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-2) (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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (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 1 of 10) (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 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 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 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 - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (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 - MariaDB / 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-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
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (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 Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
2026-09-18 10:29:24 +05:30
keshav c1e6e036d0 feat(aml): enforce review locking and sync STR case conclusions to linked alerts 2026-09-18 10:29:17 +05:30
niteshandClaude Sonnet 5 4c0512f154 feat(shares, cashier): tag GL counter entries by purpose, close DD/Share voucher cross-use
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (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 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 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 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-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
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
A cashier Receipt or clerk Collection into a GL account never named who or what it was for,
so a manager applying a share request could adopt a voucher a cashier had actually collected
for a Demand Draft, and vice versa. Adds a GLEntryType code (General/Share Collection/DD Issue,
extensible via Manage Codes) that the counter sets at save time; Share Advance Collection and
Sponsor DD issuance both check it before adopting a voucher, and also cross-check each other's
own consumption tables directly so the guard holds even for untagged legacy rows.

Also:
- Share-Collection-tagged vouchers auto-surface on Apply Share / Additional Share screens
  instead of requiring a manual GL-account search, but only when a matching client_id already
  exists on the row or the request is scoped to a specific member's screen -- a clientless GL
  receipt is never auto-attributed to whoever's screen happened to be open.
- gl_transaction_id column on m_clerk_disbursement_request captures the real GL posting a
  Collection approval makes, previously generated and discarded.
- New GET .../gl-entries endpoint exposes a receipt's actual Dr/Cr legs for detail popups.
- Additional-share tranches restricted to a single funding voucher for now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 17:39:59 +05:30
niteshandClaude Opus 5 b779d04571 fix(accounting): ledger transaction-id integrity, share GL correctness, and validation fixes
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 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-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (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-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
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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (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 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 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 Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (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-5) (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
Ledger transaction ids:
- New LedgerTransactionId generator (PREFIX-YYMMDD-NNNN, e.g. DIVRND-260916-0001) backed by a
  dedicated m_ledger_txn_sequence counter (migration 0563) that belongs to no business table, so
  no table's sequence reset can make an id repeat. Migrated 18 hand-rolled id sites (dividend,
  branch day, closure, year-end, share collection/advance) off ad hoc nanoTime()/PK-concatenation
  strings.
- ShareAccount.getShareAccountTransaction() matched a just-saved transaction by date+shares+price+
  type and took the first hit -- two identical transactions on the same day resolved to the wrong
  one and posted under the older transaction's id. Now matches by id where one exists.

Share accounting -- Cash at Vault fully retired from the share account/product lifecycle:
- New ShareAccountingSuppression lets a register that posts its own accounting tell the platform
  not to post its native version (mirrors VoucherService.withOwnVoucher). Wired into share
  withdrawal, additional-shares apply/approve/reject and account close, so every governed flow now
  posts one balanced entry against the real counterparty instead of the platform's fixed Shares
  Reference placeholder, reversed and reposted.
- Modify Application and Reject (the account's first application, pre-approval) no longer post via
  the native bridge at all -- no money has moved at that stage, so there was nothing to post.
- The native "close" command no longer force-redeems a share account's remaining holding in one
  unguarded step; closing with a balance outstanding is now refused, so redemption can only happen
  through the governed Share Withdrawal flow the frontend already restricts it to.
- "Share Reference (Cash Vault or Suspense)" is optional on the product, not mandatory: validator,
  GL mapping write/merge (including removing a stale mapping when the field is cleared on edit),
  and AccountingProcessorHelper's resolver (now throws a named error instead of NPEing when a
  product carries no mapping) all updated together. Removed from the product UI entirely (create,
  edit, preview, view).
- AdditionalShareTrancheFundingService: submit posts nothing; settle() posts the whole entry itself,
  debiting whichever GL a voucher receipt actually landed in (read back from its own posting) or the
  member's savings account, crediting Share Capital. A voucher-funded tranche whose receipts total
  more than it costs now credits the difference to the member's linked savings account -- every
  named receipt is visited, not just enough to cover the tranche, since one left untouched here was
  still marked APPLIED and its money left untraceable.
- ShareAccountDataSerializer: redeeming every share down to zero is exempt from the product's
  minimum-shares check (the check protects an account staying open with a reduced holding, not one
  being closed via a full exit); an over-payment on a voucher-funded tranche is refused at submit
  when no savings account is linked to receive it, rather than accepted and left unable to settle;
  six failWithCode(code, message) calls in the tranche validator were silently discarding their own
  message for the generic fallback text (failWithCode's second argument is a template arg, not a
  displayed string) -- switched to failWithCodeAndMessage.

Removed: ShareCollectionApiResource, a direct-cash counter-collection endpoint that read
m_share_account.cashier_collected, a column that has never existed in this schema -- no call site
in this codebase's frontend reached it, and no entry it could have produced ever has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 12:54:04 +05:30
niteshandClaude Opus 5 184d1b8cf8 feat(accounting): GL-funded DD issuance, share-withdrawal GL settlement, and ledger txn-id hardening
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 Docker Builds / build (true, docker-compose.yml, mariadb) (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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (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 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 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 - PostgreSQL / test (test-core-2) (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 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-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (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
Share withdrawal (clerk):
- Add GL and NONE settlement modes alongside the existing SAVINGS mode. A clerk
  can now credit any GL account on the CR row of a Fund Transfer instead of the
  member's linked savings account; picking the product's own Sundry account
  self-cancels, so it downgrades to NONE and the request stands on leg 1 alone.
- Post Dr Sundry / Cr chosen GL for GL mode; raise a standalone Dr Share /
  Cr Sundry voucher for NONE mode, read back from the ledger.

Sponsor DD:
- Let a clerk fund a Demand Draft by debiting a GL account directly
  (payment_method = GL) under the existing maker-checker flow, reusing the
  shared postGlOffsetEntry() so only the debit-side GL resolution differs.

Cash reports and vouchers:
- Fix the Closing Cash Report total row and opening-balance carry-forward.
- Read the real till GL back from the underlying transaction when raising
  withdrawal, closure and loan vouchers, instead of labelling every leg
  "Cash in Hand".

Ledger transaction-id hardening:
A derived transaction_id text (e.g. "SH" + share transaction id) is not
guaranteed unique if the underlying numeric id is ever reused, which a manual
sequence reset can cause. Queries that read those rows back now disambiguate,
and flag flips can no longer touch an unrelated transaction:
- Read-back sites pick the most recently posted matching rows (ORDER BY id DESC
  + LIMIT) or, where the read happens long after posting, the candidate group
  whose created_date sits closest to the owning request's actioned_at.
- Every UPDATE ... SET reversed = true/false WHERE transaction_id = ? is now
  guarded by the opposite reversed state, so a collision can only flip rows
  that are currently live.
- Covers clerk disbursement, sponsor DD, cheque book, clearing, share tranche
  funding and share withdrawal.

Migrations 0551-0554: settlement GL account and voucher_no columns for the
clerk share withdrawal, dividend claim and sponsor DD issue request tables.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 17:16:17 +05:30
keshav 521d61d6b7 feat: AML alert rule disposition, Client Behaviour Window evaluation, Risk Policy parameters & Board Resolution register
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (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 E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
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 Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (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 Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (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 Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (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 - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (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 - PostgreSQL / test (test-core-1) (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-5) (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
2026-09-16 12:50:43 +05:30
tarunmalviya 37c961f713 DDS: Agent settlement (Agent Tran), Close Party account, reports (Supplementary-DDS/Master Data Export/Close Register), Party-Savings link, product category field
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (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 Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
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 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 1 of 10) (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 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 - 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 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-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
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
2026-08-25 10:33:37 +05:30
636 changed files with 67691 additions and 6734 deletions
+1 -1
View File
@@ -154,7 +154,7 @@ gitVersioning.apply {
}
}
rev {
version = '${describe.tag.version.major}.${describe.tag.version.minor.next}.0-SNAPSHOT'
version = '1.15.0-SNAPSHOT'
}
}
@@ -279,9 +279,15 @@ public final class ProductToGLAccountMappingFromApiJsonDeserializer {
if (AccountingValidations.isCashBasedAccounting(accountingRuleType)) {
// Optional, not mandatory like the three below: it names an account for cash physically
// received at the counter for a share purchase, which every register in this bank now
// books against the account the money actually reached instead. Left absent, a product
// simply carries no such mapping, and any code path that still needs one (a genuine
// over-the-counter collection) fails with a named "GL not mapped" error rather than
// silently assuming the till.
final Long shareReferenceId = this.fromApiJsonHelper.extractLongNamed(SharesProductAccountingParams.SHARES_REFERENCE.getValue(),
element);
baseDataValidator.reset().parameter(SharesProductAccountingParams.SHARES_REFERENCE.getValue()).value(shareReferenceId).notNull()
baseDataValidator.reset().parameter(SharesProductAccountingParams.SHARES_REFERENCE.getValue()).value(shareReferenceId)
.integerGreaterThanZero();
final Long shareSuspenseId = this.fromApiJsonHelper.extractLongNamed(SharesProductAccountingParams.SHARES_SUSPENSE.getValue(),
@@ -38,6 +38,8 @@ public interface ProductToGLAccountMappingReadPlatformService {
Map<String, Object> fetchAccountMappingDetailsForSavingsProduct(Long savingsProductId, Integer accountingType);
Map<String, Object> fetchAccountMappingDetailsForCurrentProduct(Long currentProductId, Integer accountingType);
List<PaymentTypeToGLAccountMapper> fetchPaymentTypeToFundSourceMappingsForSavingsProduct(Long savingsProductId);
List<ChargeToGLAccountMapper> fetchFeeToIncomeAccountMappingsForSavingsProduct(Long savingsProductId);
@@ -202,6 +202,20 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
return accountMappingDetails;
}
/** A current product's mappings, which live under their own CURRENT product type, laid out as a savings product's. */
@Override
public Map<String, Object> fetchAccountMappingDetailsForCurrentProduct(final Long currentProductId, final Integer accountingType) {
final List<ProductToGLAccountMapping> mappings = productToGLAccountMappingRepository.findAllRegularMappings(currentProductId,
PortfolioProductType.CURRENT.getValue());
if (AccountingValidations.isCashBasedAccounting(accountingType)) {
return setCashSavingsProductToGLAccountMaps(mappings);
}
if (AccountingValidations.isAccrualPeriodicBasedAccounting(accountingType)) {
return setAccrualPeriodicSavingsProductToGLAccountMaps(mappings);
}
return null;
}
@Override
public List<PaymentTypeToGLAccountMapper> fetchPaymentTypeToFundSourceMappingsForLoanProduct(final Long loanProductId) {
return fetchPaymentTypeToFundSourceMappings(PortfolioProductType.LOAN, loanProductId);
@@ -463,6 +477,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_CONTROL.getValue(), glAccountData);
} else if (glAccountForSavings.equals(CashAccountsForSavings.TRANSFERS_SUSPENSE)) {
accountMappingDetails.put(SavingProductAccountingDataParams.TRANSFERS_SUSPENSE.getValue(), glAccountData);
} else if (glAccountForSavings.equals(CashAccountsForSavings.INTEREST_PAYABLE)) {
accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_PAYABLE.getValue(), glAccountData);
// Income
} else if (glAccountForSavings.equals(CashAccountsForSavings.INCOME_FROM_FEES)) {
accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_FEES.getValue(), glAccountData);
@@ -264,6 +264,13 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa
savingsProductId, CashAccountsForSavings.ESCHEAT_LIABILITY.getValue(), changes);
createOrmergeSavingsToExpenseAccountMappingChanges(element, SavingProductAccountingParams.ROUNDING_ADJUSTMENT.getValue(),
savingsProductId, CashAccountsForSavings.ROUNDING_ADJUSTMENT.getValue(), changes);
// Interest Payable is editable on cash too, and optional. Interest posting routes
// through it on either basis, so a product can name one to carry interest declared
// but not yet credited; a product that names none still posts straight to Savings
// Control. Created-or-merged because a product configured before this row existed
// has no mapping to merge into.
createOrmergeSavingsToLiabilityAccountMappingChanges(element, SavingProductAccountingParams.INTEREST_PAYABLE.getValue(),
savingsProductId, AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), changes);
break;
case ACCRUAL_PERIODIC:
// asset
@@ -27,6 +27,7 @@ import org.apache.fineract.accounting.common.AccountingRuleType;
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepository;
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
@@ -166,9 +167,9 @@ public class ShareProductToGLAccountMappingHelper extends ProductToGLAccountMapp
case NONE:
break;
case CASH_BASED:
// asset
mergeSharesToAssetAccountMappingChanges(element, SharesProductAccountingParams.SHARES_REFERENCE.getValue(), sharesProductId,
CashAccountsForShares.SHARES_REFERENCE.getValue(), CashAccountsForShares.SHARES_REFERENCE.toString(), changes);
// asset -- the one optional mapping here, so unlike the three below it needs its own
// handling for the case the field is now absent (see mergeOrRemoveSharesReferenceMapping).
mergeOrRemoveSharesReferenceMapping(element, sharesProductId, changes);
// income
mergeSharesToIncomeAccountMappingChanges(element, SharesProductAccountingParams.INCOME_FROM_FEES.getValue(),
@@ -194,4 +195,35 @@ public class ShareProductToGLAccountMappingHelper extends ProductToGLAccountMapp
public void deleteSharesProductToGLAccountMapping(final Long sharesProductId) {
deleteProductToGLAccountMapping(sharesProductId, PortfolioProductType.SHARES);
}
/**
* Handles the one optional share-product mapping, Shares Reference, on an edit that does not change
* the accounting rule.
*
* <p>
* The other three mappings (Suspense, Income From Fees, Equity) are mandatory, so
* {@code mergeProductToAccountMappingChanges} only ever needs to handle "present and possibly
* changed" for them -- a null value there cannot happen without failing validation first. Shares
* Reference can legitimately be absent, and the shared merge helper (used across loan, savings and
* share products alike) only handles "present": it silently leaves an existing mapping row alone
* when the incoming value is null, rather than clearing it. Editing this one field out of an
* existing product would otherwise leave the product still carrying its old GL underneath, invisible
* on the edit form but still resolvable by any code path that looks it up directly.
*/
private void mergeOrRemoveSharesReferenceMapping(final JsonElement element, final Long sharesProductId,
final Map<String, Object> changes) {
final Long shareReferenceId = this.fromApiJsonHelper.extractLongNamed(SharesProductAccountingParams.SHARES_REFERENCE.getValue(),
element);
if (shareReferenceId != null) {
mergeSharesToAssetAccountMappingChanges(element, SharesProductAccountingParams.SHARES_REFERENCE.getValue(), sharesProductId,
CashAccountsForShares.SHARES_REFERENCE.getValue(), CashAccountsForShares.SHARES_REFERENCE.toString(), changes);
return;
}
final ProductToGLAccountMapping existing = this.accountMappingRepository.findCoreProductToFinAccountMapping(sharesProductId,
PortfolioProductType.SHARES.getValue(), CashAccountsForShares.SHARES_REFERENCE.getValue());
if (existing != null) {
this.accountMappingRepository.delete(existing);
changes.put(SharesProductAccountingParams.SHARES_REFERENCE.getValue(), null);
}
}
}
@@ -33,4 +33,6 @@ public interface TellerRepository extends JpaRepository<Teller, Long>, JpaSpecif
Collection<Teller> findTellerByOfficeId(Long officeId);
long countByDeletedFalse();
long countByOfficeIdAndDeletedFalse(Long officeId);
}
@@ -58,4 +58,9 @@ public class TellerRepositoryWrapper {
public boolean existsAny() {
return this.repository.countByDeletedFalse() > 0;
}
/** Whether the branch already has a (not deleted) teller — each branch keeps one vault of its own. */
public boolean existsInOffice(final Long officeId) {
return this.repository.countByOfficeIdAndDeletedFalse(officeId) > 0;
}
}
@@ -28,4 +28,9 @@ public class TellerAlreadyExistsException extends AbstractPlatformDomainRuleExce
public TellerAlreadyExistsException() {
super(ERROR_MESSAGE_CODE, DEFAULT_ERROR_MESSAGE);
}
public TellerAlreadyExistsException(final String officeName) {
super(ERROR_MESSAGE_CODE, "Cannot create teller: " + officeName + " already has a teller. Each branch has one teller.",
officeName);
}
}
@@ -270,6 +270,7 @@ public final class AccountingConstants {
INCOME_FROM_INTEREST(12), //
LOSSES_WRITTEN_OFF(13), //
ESCHEAT_LIABILITY(14), //
INTEREST_PAYABLE(17), //
/**
* Where the paise dropped from a cash closure are absorbed.
*
@@ -157,19 +157,35 @@ public class CommandSourceService {
}
if ("GLACCOUNT".equalsIgnoreCase(commandSource.getEntityName())
|| "CBSBASEINTERESTRATE".equalsIgnoreCase(commandSource.getEntityName())
|| "LOANBASEINTERESTRATE".equalsIgnoreCase(commandSource.getEntityName())
|| "DELINQUENCY_BUCKET".equalsIgnoreCase(commandSource.getEntityName())
|| "DELINQUENCY_RANGE".equalsIgnoreCase(commandSource.getEntityName())
|| "CHARGE".equalsIgnoreCase(commandSource.getEntityName())
|| "LOANPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|| "SAVINGSPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|| "CURRENTPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|| "SHAREPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|| "FIXEDDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|| "RECURRINGDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|| "USER".equalsIgnoreCase(commandSource.getEntityName())
|| "ROLE".equalsIgnoreCase(commandSource.getEntityName())) {
|| "ROLE".equalsIgnoreCase(commandSource.getEntityName())
|| isDdsEntity(commandSource.getEntityName())) {
return false;
}
return true;
}
/**
* The DDS module's maker-checker entities. A super user's DDS work goes to a checker like anyone
* else's: DDS moves customers' money, and every such move is to be approved by a second person.
*/
public static boolean isDdsEntity(final String entityName) {
return entityName != null && switch (entityName.toUpperCase()) {
case "PARTY", "AGENT", "PARTYTRANSACTION", "DDSACCOUNTTYPE", "COMMISSIONSLAB", "DDSBATCH", "DDSCOLLECTIONENTRY" -> true;
default -> false;
};
}
private void sanitizeJson(@NonNull CommandSource commandSource, Set<String> sanitizeKeys) {
if (sanitizeKeys == null || sanitizeKeys.isEmpty()) {
return;
@@ -664,6 +664,167 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder createAgent() {
this.actionName = "CREATE";
this.entityName = "AGENT";
this.entityId = null;
this.href = "/dds/agents/template";
return this;
}
public CommandWrapperBuilder updateAgent(final Long agentId) {
this.actionName = "UPDATE";
this.entityName = "AGENT";
this.entityId = agentId;
this.href = "/dds/agents/" + agentId;
return this;
}
public CommandWrapperBuilder closeAgent(final Long agentId) {
this.actionName = "CLOSE";
this.entityName = "AGENT";
this.entityId = agentId;
this.href = "/dds/agents/" + agentId + "?command=close";
return this;
}
public CommandWrapperBuilder createParty() {
this.actionName = "CREATE";
this.entityName = "PARTY";
this.entityId = null;
this.href = "/dds/parties/template";
return this;
}
public CommandWrapperBuilder updateParty(final Long partyId) {
this.actionName = "UPDATE";
this.entityName = "PARTY";
this.entityId = partyId;
this.href = "/dds/parties/" + partyId;
return this;
}
public CommandWrapperBuilder closeParty(final Long partyId) {
this.actionName = "CLOSE";
this.entityName = "PARTY";
this.entityId = partyId;
this.href = "/dds/parties/" + partyId + "?command=close";
return this;
}
/**
* Pays a matured DDS account out and closes it. A separate action from CLOSE because it moves
* money, and so is worth its own permission and its own line in the audit trail.
*/
public CommandWrapperBuilder payOutParty(final Long partyId) {
this.actionName = "PAYOUT";
this.entityName = "PARTY";
this.entityId = partyId;
this.href = "/dds/parties/" + partyId + "?command=payout";
return this;
}
/**
* Closes a DDS account before its term is up, settling the shortfall against the customer or the
* agent. Separate from both CLOSE and PAYOUT because it is the only one of the three that changes
* what a customer is owed, so a bank will want it approved by someone other than whoever raised it.
*/
public CommandWrapperBuilder prematureCloseParty(final Long partyId) {
this.actionName = "PREMATURECLOSE";
this.entityName = "PARTY";
this.entityId = partyId;
this.href = "/dds/parties/" + partyId + "?command=prematureclose";
return this;
}
public CommandWrapperBuilder prematureCloseOverrideParty(final Long partyId) {
this.actionName = "PREMATURECLOSEOVERRIDE";
this.entityName = "PARTY";
this.entityId = partyId;
this.href = "/dds/parties/" + partyId + "?command=prematurecloseoverride";
return this;
}
public CommandWrapperBuilder createDdsAccountType() {
this.actionName = "CREATE";
this.entityName = "DDSACCOUNTTYPE";
this.entityId = null;
this.href = "/dds/accounttypes/template";
return this;
}
public CommandWrapperBuilder updateDdsAccountType(final Long accountTypeId) {
this.actionName = "UPDATE";
this.entityName = "DDSACCOUNTTYPE";
this.entityId = accountTypeId;
this.href = "/dds/accounttypes/" + accountTypeId;
return this;
}
public CommandWrapperBuilder createPartyTransaction() {
this.actionName = "CREATE";
this.entityName = "PARTYTRANSACTION";
this.entityId = null;
this.href = "/dds/partytransactions/template";
return this;
}
public CommandWrapperBuilder createDdsCollectionEntries() {
this.actionName = "CREATE";
this.entityName = "DDSCOLLECTIONENTRY";
this.entityId = null;
this.href = "/dds/collectionentries/template";
return this;
}
public CommandWrapperBuilder cancelDdsCollectionEntry(final Long entryId) {
this.actionName = "CANCEL";
this.entityName = "DDSCOLLECTIONENTRY";
this.entityId = entryId;
this.href = "/dds/collectionentries/" + entryId;
return this;
}
public CommandWrapperBuilder settleDdsCollectionEntries() {
this.actionName = "SETTLE";
this.entityName = "DDSCOLLECTIONENTRY";
this.entityId = null;
this.href = "/dds/collectionentries/settle";
return this;
}
public CommandWrapperBuilder createCommissionSlabs() {
this.actionName = "CREATE";
this.entityName = "COMMISSIONSLAB";
this.entityId = null;
this.href = "/dds/commissionslabs/template";
return this;
}
public CommandWrapperBuilder updateCommissionSlab(final Long slabId) {
this.actionName = "UPDATE";
this.entityName = "COMMISSIONSLAB";
this.entityId = slabId;
this.href = "/dds/commissionslabs/" + slabId;
return this;
}
public CommandWrapperBuilder generateDdsBatchRun() {
this.actionName = "GENERATE";
this.entityName = "DDSBATCH";
this.entityId = null;
this.href = "/dds/batchruns/template";
return this;
}
public CommandWrapperBuilder postDdsBatchRun(final Long batchRunId) {
this.actionName = "POST";
this.entityName = "DDSBATCH";
this.entityId = batchRunId;
this.href = "/dds/batchruns/" + batchRunId;
return this;
}
public CommandWrapperBuilder deleteOffice(final Long officeId) {
this.actionName = "DELETE";
this.entityName = "OFFICE";
@@ -756,6 +917,14 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder deleteFund(final Long fundId) {
this.actionName = "DELETE";
this.entityName = "FUND";
this.entityId = fundId;
this.href = "/funds/" + fundId;
return this;
}
public CommandWrapperBuilder createReport() {
this.actionName = "CREATE";
this.entityName = "REPORT";
@@ -884,6 +1053,70 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder createPaymentType() {
this.actionName = "CREATE";
this.entityName = "PAYMENTTYPE";
this.entityId = null;
this.href = "/paymenttypes/template";
return this;
}
public CommandWrapperBuilder updatePaymentType(final Long paymentTypeId) {
this.actionName = "UPDATE";
this.entityName = "PAYMENTTYPE";
this.entityId = paymentTypeId;
this.href = "/paymenttypes/" + paymentTypeId;
return this;
}
public CommandWrapperBuilder deletePaymentType(final Long paymentTypeId) {
this.actionName = "DELETE";
this.entityName = "PAYMENTTYPE";
this.entityId = paymentTypeId;
this.href = "/paymenttypes/" + paymentTypeId;
return this;
}
public CommandWrapperBuilder createSponsorBank() {
this.actionName = "CREATE";
this.entityName = "SPONSORBANK";
this.entityId = null;
this.href = "/sponsorbanks";
return this;
}
public CommandWrapperBuilder updateSponsorBank(final Long sponsorBankId) {
this.actionName = "UPDATE";
this.entityName = "SPONSORBANK";
this.entityId = sponsorBankId;
this.href = "/sponsorbanks/" + sponsorBankId;
return this;
}
public CommandWrapperBuilder updateSponsorDdGlMapping() {
this.actionName = "UPDATE";
this.entityName = "SPONSORDDGLMAPPING";
this.entityId = null;
this.href = "/sponsordd/gl-mapping";
return this;
}
public CommandWrapperBuilder updateInstitutionProfile() {
this.actionName = "UPDATE";
this.entityName = "INSTITUTIONPROFILE";
this.entityId = null;
this.href = "/institutionprofile";
return this;
}
public CommandWrapperBuilder updateBranchProfile(final Long officeId) {
this.actionName = "UPDATE";
this.entityName = "BRANCHPROFILE";
this.entityId = officeId;
this.href = "/branchprofile/" + officeId;
return this;
}
public CommandWrapperBuilder createLoanProduct() {
this.actionName = "CREATE";
this.entityName = "LOANPRODUCT";
@@ -2227,6 +2460,14 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder createLoanBaseInterestRate() {
this.actionName = "CREATE";
this.entityName = "LOANBASEINTERESTRATE";
this.entityId = null;
this.href = "/loanproducts/baseinterestrates";
return this;
}
public CommandWrapperBuilder updateFixedDepositProduct(final Long productId) {
this.actionName = "UPDATE";
this.entityName = "FIXEDDEPOSITPRODUCT";
@@ -3085,6 +3326,15 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder extendTenureRecurringDepositAccount(final Long accountId) {
this.actionName = "EXTENDTENURE";
this.entityName = "RECURRINGDEPOSITACCOUNT";
this.entityId = accountId;
this.savingsId = accountId;
this.href = "/recurringdepositaccounts/" + accountId + "?command=extendTenure";
return this;
}
public CommandWrapperBuilder prematureCloseRecurringDepositAccount(final Long accountId) {
this.actionName = "PREMATURECLOSE";
this.entityName = "RECURRINGDEPOSITACCOUNT";
@@ -3390,6 +3640,13 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder updateCurrency() {
this.actionName = "UPDATE";
this.entityName = "CURRENCY";
this.href = "/currencies";
return this;
}
public CommandWrapperBuilder updateExternalServiceProperties(final String externalServiceName) {
this.actionName = "UPDATE";
this.entityName = "EXTERNALSERVICES";
@@ -4730,4 +4987,36 @@ public class CommandWrapperBuilder {
this.href = "/loans/" + loanId + "/originators/" + originatorId;
return this;
}
public CommandWrapperBuilder createSurvey() {
this.actionName = "CREATE";
this.entityName = "SURVEY";
this.entityId = null;
this.href = "/surveys";
return this;
}
public CommandWrapperBuilder updateSurvey(final Long surveyId) {
this.actionName = "UPDATE";
this.entityName = "SURVEY";
this.entityId = surveyId;
this.href = "/surveys/" + surveyId;
return this;
}
public CommandWrapperBuilder activateSurvey(final Long surveyId) {
this.actionName = "ACTIVATE";
this.entityName = "SURVEY";
this.entityId = surveyId;
this.href = "/surveys/" + surveyId;
return this;
}
public CommandWrapperBuilder deactivateSurvey(final Long surveyId) {
this.actionName = "DEACTIVATE";
this.entityName = "SURVEY";
this.entityId = surveyId;
this.href = "/surveys/" + surveyId;
return this;
}
}
@@ -123,7 +123,9 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
AppUser appUser = this.context.authenticatedUser();
String permissionCode = commandSource.getPermissionCode();
appUser.validateHasCheckerPermissionTo(permissionCode);
if (!configurationService.isSameMakerCheckerEnabled() && !appUser.isCheckerSuperUser()) {
// A DDS entry is never checked by the user who made it, super user or not - see CommandSourceService.isDdsEntity.
if (!configurationService.isSameMakerCheckerEnabled()
&& (!appUser.isCheckerSuperUser() || CommandSourceService.isDdsEntity(commandSource.getEntityName()))) {
AppUser maker = commandSource.getMaker();
if (maker == null) {
throw new UnsupportedCommandException(permissionCode, "Maker user is missing.");
@@ -32,6 +32,7 @@ public final class GlobalConfigurationConstants {
public static final String FORCE_PASSWORD_RESET_DAYS = "force-password-reset-days";
public static final String GRACE_ON_PENALTY_POSTING = "grace-on-penalty-posting";
public static final String SAVINGS_INTEREST_POSTING_CURRENT_PERIOD_END = "savings-interest-posting-current-period-end";
public static final String FD_INTEREST_POSTING_APPROVAL = "fd-interest-posting-approval";
public static final String FINANCIAL_YEAR_BEGINNING_MONTH = "financial-year-beginning-month";
public static final String MIN_CLIENTS_IN_GROUP = "min-clients-in-group";
public static final String MAX_CLIENTS_IN_GROUP = "max-clients-in-group";
@@ -69,6 +69,12 @@ public interface ConfigurationDomainService {
boolean isSavingsInterestPostingAtCurrentPeriodEnd();
/**
* Whether fixed deposit interest is reported and approved before it is posted, instead of being
* posted directly by the nightly job.
*/
boolean isFixedDepositInterestPostingApprovalEnabled();
Integer retrieveFinancialYearBeginningMonth();
Integer retrieveMinAllowedClientsInGroup();
@@ -505,6 +505,7 @@ public class FineractProperties {
private FineractTransactionProcessorItemProperties heavensFamily;
private FineractTransactionProcessorItemProperties interestPrincipalPenaltiesFees;
private FineractTransactionProcessorItemProperties principalInterestPenaltiesFees;
private FineractTransactionProcessorItemProperties feesPenaltiesInterestPrincipal;
private FineractTransactionProcessorItemProperties rbiIndia;
private FineractTransactionProcessorItemProperties duePenaltyFeeInterestPrincipalInAdvancePrincipalPenaltyFeeInterest;
private FineractTransactionProcessorItemProperties duePenaltyInterestPrincipalFeeInAdvancePenaltyInterestPrincipalFee;
@@ -43,6 +43,8 @@ public enum JobName {
UPDATE_SAVINGS_DORMANT_ACCOUNTS("Update Savings Dormant Accounts"), //
APPLY_KYC_EXPIRY_RESTRICTIONS("Apply KYC Expiry Restrictions"), //
SAVINGS_DAILY_INTEREST("Savings Daily Interest Accrual And Posting"), //
SAVINGS_INTEREST_POSTING_REPORT("Generate Savings Interest Posting Report"), //
FD_INTEREST_POSTING_REPORT("Generate Fixed Deposit Interest Posting Report"), //
ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_LOANS_WITH_INCOME_POSTED_AS_TRANSACTIONS(
"Add Accrual Transactions For Loans With Income Posted As Transactions"), //
EXECUTE_REPORT_MAILING_JOBS("Execute Report Mailing Jobs"), //
@@ -72,6 +74,11 @@ public enum JobName {
OUTWARD_CLEARING_BATCH_SEND("CTS Outward Clearing Batch Send"), //
CTR_REPORT_GENERATION("AML Monthly CTR Report Generation"), //
AML_STR_WEEKLY_DIGEST("AML STR Weekly Digest"), //
POST_DDS_INTEREST("Post DDS Interest"), //
CLIENT_BEHAVIOUR_WINDOW("Client Behaviour Window Computation"), //
SAVE_RBI_FORM_A_RETURN("Save RBI Form A Return"), //
EXECUTE_RD_AUTO_DEBIT("Execute RD Auto Debit"), //
APPLY_RD_MISSED_INSTALLMENT_PENALTY("Apply RD Missed Installment Penalty"), //
; //
private final String name;
@@ -0,0 +1,99 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.agent.data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Collection;
import lombok.Builder;
import lombok.Getter;
/**
* Immutable data object representing a DDS agent and its guarantors, following the legacy Agent
* Master Entry screen. Built via the Lombok builder rather than a long positional constructor, as the
* other DDS data objects are.
*
* The commission block below is on the legacy SubAgent Master screen rather than the Agent one, and
* drives the commission batch.
*/
@Getter
@Builder(toBuilder = true)
public final class AgentData implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private final Long id;
private final Long clientId;
private final String clientName;
private final String agentCode;
private final String agentNameId;
private final String agentAddress1;
private final String agentAddress2;
private final String agentCity;
private final String agentPinCode;
private final String agentTelephone;
private final String agentSavingAccountType;
private final String agentSavingAccountNo;
private final LocalDate agentOpeningDate;
private final LocalDate agentBirthDate;
private final BigDecimal agentCurrentBalance;
private final BigDecimal agentOpeningBalance;
/**
* The branch this agent belongs to. {@code officeId} is what the screen sends back on an edit;
* {@code homeBranch} is that office's current name, read from the office row rather than from
* the agent, so a renamed branch reads correctly here without the agent being touched.
*/
private final Long officeId;
private final String homeBranch;
/**
* The ledger account this agent's collections pass through, if the bank gave them one. The id is
* what the screen sends back on an edit; the code and name are what it shows.
*/
private final Long glAccountId;
private final String glAccountCode;
private final String glAccountName;
/** Commission owed but not yet paid out, and the TDS withheld so far. Both are batch outcomes. */
private final BigDecimal commission;
private final BigDecimal tdsAmount;
/** Commission settings and the standing figures the commission run deducts. */
private final boolean commissionCalculation;
private final boolean tdsApplicable;
private final BigDecimal tdsPercent;
private final BigDecimal deductCommission;
private final BigDecimal agentDeposit;
private final BigDecimal festivalAdvance;
private final BigDecimal otherDeduction;
private final boolean active;
/** When the relationship ended, or null while the agent is on the books. */
private final LocalDate closeDate;
private final LocalDateTime createdDate;
private final LocalDateTime lastModifiedDate;
private final Collection<AgentGuarantorData> guarantors;
}
@@ -0,0 +1,59 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.agent.data;
import java.io.Serial;
import java.io.Serializable;
import lombok.Getter;
/**
* Immutable data object representing a single agent guarantor.
*/
@Getter
public final class AgentGuarantorData implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private final Long id;
private final Long clientId;
private final String guarantorName;
private final String address1;
private final String address2;
private final String city;
private final String pinCode;
private final String telephone;
public static AgentGuarantorData instance(final Long id, final Long clientId, final String guarantorName, final String address1,
final String address2, final String city, final String pinCode, final String telephone) {
return new AgentGuarantorData(id, clientId, guarantorName, address1, address2, city, pinCode, telephone);
}
private AgentGuarantorData(final Long id, final Long clientId, final String guarantorName, final String address1,
final String address2, final String city, final String pinCode, final String telephone) {
this.id = id;
this.clientId = clientId;
this.guarantorName = guarantorName;
this.address1 = address1;
this.address2 = address2;
this.city = city;
this.pinCode = pinCode;
this.telephone = telephone;
}
}
@@ -0,0 +1,523 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.agent.domain;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderBy;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.client.domain.Client;
/**
* A DDS (Daily Deposit Scheme) collection agent. Can represent either a person or, at the top of the
* hierarchy, the bank itself acting as its own agent. Holds its own settlement savings account and one
* or more guarantors.
*
* The field set follows the legacy Agent Master Entry screen. SubAgent was folded into Agent
* (2026-08-24) so the Agent now does the daily collection work directly, but the SubAgent-only fields
* that came across with it (TDS, transfer/security accounts, machine rent, collection-limit history)
* were on no screen the business uses and were read by no query, report or job, so they were dropped
* again in 0481. Two fields are kept despite not appearing on that screen, because code reads them:
* {@code commission}/{@code lastCommissionDate}, and the deduction figures the Agent Commission
* Statement reports.
*/
@Getter
@Entity
@Table(name = "m_dds_agent", uniqueConstraints = { @UniqueConstraint(columnNames = { "agent_code" }, name = "agent_code_UNIQUE") })
public class Agent extends AbstractAuditableCustom {
@ManyToOne
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@Column(name = "agent_code", length = 20, nullable = false)
private String agentCode;
@Column(name = "agent_name", length = 100, nullable = false)
private String agentName;
@Column(name = "agent_address1", length = 200, nullable = false)
private String agentAddress1;
@Column(name = "agent_address2", length = 200)
private String agentAddress2;
@Column(name = "agent_city", length = 100, nullable = false)
private String agentCity;
@Column(name = "agent_pincode", length = 20, nullable = false)
private String agentPinCode;
@Column(name = "agent_telephone", length = 20)
private String agentTelephone;
@Column(name = "agent_saving_account_type", length = 100, nullable = false)
private String agentSavingAccountType;
@Column(name = "agent_saving_account_no", length = 50, nullable = false)
private String agentSavingAccountNo;
@Column(name = "agent_opening_date", nullable = false)
private LocalDate agentOpeningDate;
@Column(name = "agent_birth_date", nullable = false)
private LocalDate agentBirthDate;
@Column(name = "agent_current_balance", scale = 6, precision = 19, nullable = false)
private BigDecimal agentCurrentBalance;
@Column(name = "agent_opening_balance", scale = 6, precision = 19, nullable = false)
private BigDecimal agentOpeningBalance;
/**
* The branch this agent belongs to, and the thing of record for it.
*
* Defaults to the agent's client's office - where the person sits on the bank's books - but is
* chosen on the screen, since an agent can be registered at one branch and collect out of
* another.
*/
@ManyToOne
@JoinColumn(name = "office_id")
private Office office;
/**
* The office's name, held alongside the office itself.
*
* Redundant by design: the batch job's branch filter and several reports match on this name, and
* were written before the office link existed. It is always written from {@link #office}, never
* on its own, so the two cannot drift apart.
*/
@Column(name = "home_branch", length = 100)
private String homeBranch;
/**
* The ledger account this agent's collections pass through, if the bank has given them one.
*
* Collections debit it on their way to the customer and settlements credit it when the cash
* arrives, so what is left standing on it is what this agent is holding and has not brought in.
* One agent to an account - shared, it would say nothing that the old single account did not.
*
* Null for the agents that predate this, and they go on using the scheme's shared agent-cash
* account exactly as before. Nothing about the route the money takes depends on which of the
* two it is.
*/
@ManyToOne
@JoinColumn(name = "gl_account_id")
private GLAccount glAccount;
/**
* Commission earned by Commission Calculation but not yet paid out. Commission Posting pays it and
* resets this to zero, so it reads as "owed to this agent right now" - which is what the legacy
* SubAgent Master screen's "Comm" box shows.
*/
@Column(name = "commission", scale = 6, precision = 19, nullable = false)
private BigDecimal commission;
/** Whether Commission Calculation covers this agent at all ("Com. Calculation" on the legacy screen). */
@Column(name = "commission_calculation", nullable = false)
private boolean commissionCalculation;
/** TDS is withheld from commission at source when the bank has it switched on for this agent. */
@Column(name = "tds_applicable", nullable = false)
private boolean tdsApplicable;
@Column(name = "tds_percent", scale = 6, precision = 19, nullable = false)
private BigDecimal tdsPercent;
/** Running total of TDS withheld, the legacy screen's "TDS Amt". */
@Column(name = "tds_amount", scale = 6, precision = 19, nullable = false)
private BigDecimal tdsAmount;
/** The four figures the Agent Commission Statement subtracts from gross commission. */
@Column(name = "deduct_commission", scale = 6, precision = 19, nullable = false)
private BigDecimal deductCommission;
@Column(name = "agent_deposit", scale = 6, precision = 19, nullable = false)
private BigDecimal agentDeposit;
@Column(name = "festival_advance", scale = 6, precision = 19, nullable = false)
private BigDecimal festivalAdvance;
@Column(name = "other_deduction", scale = 6, precision = 19, nullable = false)
private BigDecimal otherDeduction;
@Column(name = "last_commission_date")
private LocalDate lastCommissionDate;
/**
* An agent who stops collecting is deactivated, never deleted: every party collection, settlement
* and commission posting is recorded against it, so removing it would orphan its whole history.
* An inactive agent stays out of the pick lists for new collection work while its record and
* history stay intact, and it can be switched back on.
*/
@Column(name = "is_active", nullable = false)
private boolean active;
/**
* When the relationship ended, or null while the agent is on the books.
*
* Distinct from {@code active}: switching an agent off stops them collecting and has to be
* possible immediately, whereas closing is the end of the relationship and is only allowed once
* nothing is left hanging. A closed agent is never deleted - their whole collection history is
* recorded against them.
*/
@Column(name = "close_date")
private LocalDate closeDate;
@OneToMany(mappedBy = "agent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@OrderBy("id")
private List<AgentGuarantor> guarantors = new ArrayList<>();
protected Agent() {
//
}
private Agent(final Client client, final String agentCode, final String agentName, final String agentAddress1,
final String agentAddress2, final String agentCity, final String agentPinCode, final String agentTelephone,
final String agentSavingAccountType, final String agentSavingAccountNo, final LocalDate agentOpeningDate,
final LocalDate agentBirthDate, final BigDecimal agentOpeningBalance, final Office office) {
this.client = client;
this.agentCode = agentCode;
this.agentName = agentName;
this.agentAddress1 = agentAddress1;
this.agentAddress2 = StringUtils.defaultIfEmpty(agentAddress2, null);
this.agentCity = agentCity;
this.agentPinCode = agentPinCode;
this.agentTelephone = StringUtils.defaultIfEmpty(agentTelephone, null);
this.agentSavingAccountType = agentSavingAccountType;
this.agentSavingAccountNo = agentSavingAccountNo;
this.agentOpeningDate = agentOpeningDate;
this.agentBirthDate = agentBirthDate;
this.agentOpeningBalance = agentOpeningBalance == null ? BigDecimal.ZERO : agentOpeningBalance;
this.agentCurrentBalance = this.agentOpeningBalance;
setOffice(office);
this.active = true;
this.commission = BigDecimal.ZERO;
// Commission is earned unless someone says otherwise, matching the legacy screen's default.
this.commissionCalculation = true;
this.tdsApplicable = false;
this.tdsPercent = BigDecimal.ZERO;
this.tdsAmount = BigDecimal.ZERO;
this.deductCommission = BigDecimal.ZERO;
this.agentDeposit = BigDecimal.ZERO;
this.festivalAdvance = BigDecimal.ZERO;
this.otherDeduction = BigDecimal.ZERO;
}
/**
* @param office
* the branch this agent belongs to, resolved by the caller from the command's
* {@code officeId} or, failing that, from the client's own office.
*/
public static Agent fromJson(final Client client, final String agentCode, final Office office, final JsonCommand command) {
final String agentName = command.stringValueOfParameterNamed("agentNameId");
final String agentAddress1 = command.stringValueOfParameterNamed("agentAddress1");
final String agentAddress2 = command.stringValueOfParameterNamedAllowingNull("agentAddress2");
final String agentCity = command.stringValueOfParameterNamed("agentCity");
final String agentPinCode = command.stringValueOfParameterNamed("agentPinCode");
final String agentTelephone = command.stringValueOfParameterNamedAllowingNull("agentTelephone");
final String agentSavingAccountType = command.stringValueOfParameterNamed("agentSavingAccountType");
final String agentSavingAccountNo = command.stringValueOfParameterNamed("agentSavingAccountNo");
final LocalDate agentOpeningDate = command.localDateValueOfParameterNamed("agentOpeningDate");
final LocalDate agentBirthDate = command.localDateValueOfParameterNamed("agentBirthDate");
final BigDecimal agentOpeningBalance = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("agentOpeningBalance");
return new Agent(client, agentCode, agentName, agentAddress1, agentAddress2, agentCity, agentPinCode, agentTelephone,
agentSavingAccountType, agentSavingAccountNo, agentOpeningDate, agentBirthDate, agentOpeningBalance, office);
}
public void addGuarantor(final AgentGuarantor guarantor) {
guarantor.setAgent(this);
this.guarantors.add(guarantor);
}
public void resetGuarantors() {
this.guarantors.clear();
}
/**
* Commission Calculation: adds what this agent earned to the amount owed to them, withholds the
* TDS that went with it, and stamps the date commission is now calculated through - so the next
* Commission Calculation only covers collections since then, and re-running it for the same
* period finds nothing left to earn.
*/
public void accrueCommission(final BigDecimal netAmount, final BigDecimal tdsWithheld, final LocalDate throughDate) {
this.commission = this.commission.add(netAmount);
this.tdsAmount = this.tdsAmount.add(tdsWithheld);
this.lastCommissionDate = throughDate;
}
/**
* Commission Posting: pays the given amount out of what is owed and returns what remains owed.
*
* The amount is the one the run previewed rather than whatever is owed at this moment, so what
* gets paid is always what was seen and approved. Since it comes off the owed figure, a second
* posting of the same commission finds nothing left to pay.
*/
public BigDecimal payOutCommission(final BigDecimal amount) {
this.commission = this.commission.subtract(amount);
return this.commission;
}
/**
* Charges the agent for an account of theirs that was closed before its term was up.
*
* Booked against {@code deductCommission} rather than taken straight off {@code commission},
* because what is owed at this moment may be nothing - commission is paid out in runs, and an
* agent who was paid yesterday would otherwise escape the charge entirely. Held as a deduction it
* is carried until there is commission to take it from, and it shows on the Agent Commission
* Statement between gross and take-home, where an agent can see what was charged and why.
*/
public BigDecimal chargePrematureClosure(final BigDecimal amount) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
return this.deductCommission;
}
this.deductCommission = this.deductCommission.add(amount);
return this.deductCommission;
}
/**
* The four figures the Agent Commission Statement subtracts between gross commission and the
* agent's take-home total. They are standing amounts on the agent, not per-run ones, so a run
* deducts whatever they hold when it is calculated.
*/
public BigDecimal totalDeductions() {
return this.deductCommission.add(this.agentDeposit).add(this.festivalAdvance).add(this.otherDeduction);
}
public boolean isClosed() {
return this.closeDate != null;
}
/** Cash collected from customers that has not yet reached a branch. */
public boolean holdsCash() {
return this.agentCurrentBalance.compareTo(BigDecimal.ZERO) != 0;
}
/** Commission earned but not yet paid out. */
public boolean isOwedCommission() {
return this.commission.compareTo(BigDecimal.ZERO) != 0;
}
/**
* Ends the relationship. The caller checks first that no accounts are still assigned; the two
* money conditions are checked here, where the figures live.
*
* Closing also switches the agent off, since an agent who is no longer on the books cannot be
* collecting either.
*/
public void close(final LocalDate closeDate) {
this.closeDate = closeDate;
this.active = false;
}
/**
* Cash this agent has collected from parties but not yet handed over to a branch (an Agent Tran
* settlement). Raised by a Party Tran collection, lowered by an Agent Tran settlement.
*/
public BigDecimal credit(final BigDecimal amount) {
this.agentCurrentBalance = this.agentCurrentBalance.add(amount);
return this.agentCurrentBalance;
}
public BigDecimal settle(final BigDecimal amount) {
this.agentCurrentBalance = this.agentCurrentBalance.subtract(amount);
return this.agentCurrentBalance;
}
public void changeClient(final Client newClient) {
this.client = newClient;
}
/**
* Moves the agent to a branch, writing the office's name into {@code homeBranch} in the same
* step so the name never has to be set by hand and never lags behind the office.
*/
public void setOffice(final Office office) {
this.office = office;
this.homeBranch = office == null ? null : StringUtils.defaultIfEmpty(office.getName(), null);
}
public Long officeId() {
return this.office == null ? null : this.office.getId();
}
/**
* Points this agent's collections at a ledger account, or back at the scheme's shared one.
*
* Whether the account may be used at all - that it exists, is an asset account, is not disabled,
* and is not already another agent's - is settled before this is called. By the time the agent
* holds it, the question is closed.
*/
public void setGlAccount(final GLAccount glAccount) {
this.glAccount = glAccount;
}
public Long glAccountId() {
return this.glAccount == null ? null : this.glAccount.getId();
}
public Long clientId() {
return this.client.getId();
}
public Map<String, Object> update(final JsonCommand command) {
final Map<String, Object> actualChanges = new LinkedHashMap<>(10);
if (command.isChangeInLongParameterNamed("clientId", this.client.getId())) {
actualChanges.put("clientId", command.longValueOfParameterNamed("clientId"));
}
if (command.isChangeInStringParameterNamed("agentNameId", this.agentName)) {
final String newValue = command.stringValueOfParameterNamed("agentNameId");
actualChanges.put("agentNameId", newValue);
this.agentName = newValue;
}
if (command.isChangeInStringParameterNamed("agentAddress1", this.agentAddress1)) {
final String newValue = command.stringValueOfParameterNamed("agentAddress1");
actualChanges.put("agentAddress1", newValue);
this.agentAddress1 = newValue;
}
if (command.isChangeInStringParameterNamed("agentAddress2", this.agentAddress2)) {
final String newValue = command.stringValueOfParameterNamed("agentAddress2");
actualChanges.put("agentAddress2", newValue);
this.agentAddress2 = StringUtils.defaultIfEmpty(newValue, null);
}
if (command.isChangeInStringParameterNamed("agentCity", this.agentCity)) {
final String newValue = command.stringValueOfParameterNamed("agentCity");
actualChanges.put("agentCity", newValue);
this.agentCity = newValue;
}
if (command.isChangeInStringParameterNamed("agentPinCode", this.agentPinCode)) {
final String newValue = command.stringValueOfParameterNamed("agentPinCode");
actualChanges.put("agentPinCode", newValue);
this.agentPinCode = newValue;
}
if (command.isChangeInStringParameterNamed("agentTelephone", this.agentTelephone)) {
final String newValue = command.stringValueOfParameterNamed("agentTelephone");
actualChanges.put("agentTelephone", newValue);
this.agentTelephone = StringUtils.defaultIfEmpty(newValue, null);
}
if (command.isChangeInStringParameterNamed("agentSavingAccountType", this.agentSavingAccountType)) {
final String newValue = command.stringValueOfParameterNamed("agentSavingAccountType");
actualChanges.put("agentSavingAccountType", newValue);
this.agentSavingAccountType = newValue;
}
if (command.isChangeInStringParameterNamed("agentSavingAccountNo", this.agentSavingAccountNo)) {
final String newValue = command.stringValueOfParameterNamed("agentSavingAccountNo");
actualChanges.put("agentSavingAccountNo", newValue);
this.agentSavingAccountNo = newValue;
}
if (command.isChangeInDateParameterNamed("agentOpeningDate", this.agentOpeningDate)) {
actualChanges.put("agentOpeningDate", command.stringValueOfParameterNamed("agentOpeningDate"));
this.agentOpeningDate = command.localDateValueOfParameterNamed("agentOpeningDate");
}
if (command.isChangeInDateParameterNamed("agentBirthDate", this.agentBirthDate)) {
actualChanges.put("agentBirthDate", command.stringValueOfParameterNamed("agentBirthDate"));
this.agentBirthDate = command.localDateValueOfParameterNamed("agentBirthDate");
}
if (command.isChangeInBigDecimalParameterNamed("agentOpeningBalance", this.agentOpeningBalance)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("agentOpeningBalance");
actualChanges.put("agentOpeningBalance", newValue);
// The current balance is the opening balance plus collections less settlements, so
// correcting the opening figure has to move the current one by the same amount.
// Setting it alone would leave the two contradicting each other for good, since nothing
// else ever recomputes the current balance from scratch.
this.agentCurrentBalance = this.agentCurrentBalance.add(newValue.subtract(this.agentOpeningBalance));
this.agentOpeningBalance = newValue;
actualChanges.put("agentCurrentBalance", this.agentCurrentBalance);
}
if (command.isChangeInBooleanParameterNamed("active", this.active)) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("active");
actualChanges.put("active", newValue);
this.active = newValue;
}
// Commission settings and the standing deductions. The running totals the batch maintains -
// `commission` and `tdsAmount` - are deliberately not editable here: they are the outcome of
// posted runs, and typing over them would put the agent's balance out of step with the runs
// that produced it.
if (command.isChangeInBooleanParameterNamed("commissionCalculation", this.commissionCalculation)) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("commissionCalculation");
actualChanges.put("commissionCalculation", newValue);
this.commissionCalculation = newValue;
}
if (command.isChangeInBooleanParameterNamed("tdsApplicable", this.tdsApplicable)) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("tdsApplicable");
actualChanges.put("tdsApplicable", newValue);
this.tdsApplicable = newValue;
}
decimalChange(command, actualChanges, "tdsPercent", this.tdsPercent, v -> this.tdsPercent = v);
decimalChange(command, actualChanges, "deductCommission", this.deductCommission, v -> this.deductCommission = v);
decimalChange(command, actualChanges, "agentDeposit", this.agentDeposit, v -> this.agentDeposit = v);
decimalChange(command, actualChanges, "festivalAdvance", this.festivalAdvance, v -> this.festivalAdvance = v);
decimalChange(command, actualChanges, "otherDeduction", this.otherDeduction, v -> this.otherDeduction = v);
// A rate means nothing while TDS is switched off, so it is not left behind to be applied by a
// later switch-on nobody connected to it.
if (!this.tdsApplicable) {
this.tdsPercent = BigDecimal.ZERO;
}
return actualChanges;
}
private void decimalChange(final JsonCommand command, final Map<String, Object> actualChanges, final String paramName,
final BigDecimal existingValue, final Consumer<BigDecimal> setter) {
if (command.isChangeInBigDecimalParameterNamed(paramName, existingValue)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(paramName);
actualChanges.put(paramName, newValue);
setter.accept(newValue == null ? BigDecimal.ZERO : newValue);
}
}
}
@@ -0,0 +1,87 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.agent.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
/**
* A single guarantor recorded against an {@link Agent} (or, reused with the same shape, against a
* SubAgent). Always created/replaced as part of its parent agent's own create/update command - it has
* no independent API of its own.
*
* Audited in its own right even so: a guarantor is who the bank goes to if an agent's collections go
* missing, and a timestamp on the agent saying something changed does not say that the person
* standing behind them was swapped, nor by whom.
*/
@Getter
@Entity
@Table(name = "m_dds_agent_guarantor")
public class AgentGuarantor extends AbstractAuditableCustom {
@ManyToOne
@JoinColumn(name = "agent_id", nullable = false)
private Agent agent;
@Column(name = "client_id", nullable = false)
private Long clientId;
@Column(name = "guarantor_name", length = 100, nullable = false)
private String guarantorName;
@Column(name = "address1", length = 200, nullable = false)
private String address1;
@Column(name = "address2", length = 200)
private String address2;
@Column(name = "city", length = 100, nullable = false)
private String city;
@Column(name = "pin_code", length = 20, nullable = false)
private String pinCode;
@Column(name = "telephone", length = 20)
private String telephone;
protected AgentGuarantor() {
//
}
public AgentGuarantor(final Long clientId, final String guarantorName, final String address1, final String address2,
final String city, final String pinCode, final String telephone) {
this.clientId = clientId;
this.guarantorName = guarantorName;
this.address1 = address1;
this.address2 = StringUtils.defaultIfEmpty(address2, null);
this.city = city;
this.pinCode = pinCode;
this.telephone = StringUtils.defaultIfEmpty(telephone, null);
}
void setAgent(final Agent agent) {
this.agent = agent;
}
}
@@ -0,0 +1,39 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.agent.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when an agent is closed while their books are not square.
*
* Closing is the end of the relationship, so it is refused until nothing is left hanging: no accounts
* still being collected from, no cash held that has not reached a branch, and no commission earned but
* unpaid. Each of those is money or a customer that would be left with no one responsible for it.
*
* Deactivating is deliberately not guarded this way. Switching an agent off has to be possible the
* moment the bank decides it - if it waited for the books to be squared, an agent the bank wanted
* stopped would keep collecting while the paperwork caught up.
*/
public class AgentCannotBeClosedException extends AbstractPlatformDomainRuleException {
public AgentCannotBeClosedException(final String agentCode, final String reason) {
super("error.msg.dds.agent.cannot.be.closed", "Agent " + agentCode + " cannot be closed: " + reason, agentCode, reason);
}
}
@@ -0,0 +1,32 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.agent.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when an agent is listed as one of its own guarantors - a guarantee is only worth something
* coming from someone other than the agent it backs.
*/
public class AgentCannotGuaranteeItselfException extends AbstractPlatformDomainRuleException {
public AgentCannotGuaranteeItselfException(final Long clientId) {
super("error.msg.agent.cannot.guarantee.itself", "An agent cannot be its own guarantor", clientId);
}
}
@@ -0,0 +1,37 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.agent.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when a commission run cannot pay an agent because their account will not take the credit.
*
* The account is checked when it is chosen, so reaching this means something changed afterwards -
* most likely the account was closed. Failing here rolls the whole run back rather than paying the
* other agents and leaving this one silently unpaid: a commission run that has half happened is
* worse to unpick than one that has not happened at all.
*/
public class AgentCommissionAccountUnusableException extends AbstractPlatformDomainRuleException {
public AgentCommissionAccountUnusableException(final String agentCode, final String reason) {
super("error.msg.dds.agent.commission.account.unusable",
"Commission for agent " + agentCode + " could not be paid: " + reason + ".", agentCode, reason);
}
}
@@ -0,0 +1,38 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.agent.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when the ledger account named for an agent could not carry that agent's cash.
*
* Refused when it is chosen rather than when it is first posted to. An account that cannot take the
* entry fails the whole collection, at the counter, with a customer waiting - and the person who
* picked it is not the person who finds out.
*/
public class AgentGlAccountUnusableException extends AbstractPlatformDomainRuleException {
public AgentGlAccountUnusableException(final String glCode, final String reason) {
super("error.msg.dds.agent.gl.account.unusable",
"Ledger account " + glCode + " cannot be used for this agent: " + reason
+ ". Choose a detail asset account that no other agent already holds.",
glCode, reason);
}
}
@@ -0,0 +1,40 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.agent.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException;
/**
* A {@link RuntimeException} thrown when a DDS agent resource is not found.
*/
public class AgentNotFoundException extends AbstractPlatformResourceNotFoundException {
public AgentNotFoundException(final Long id) {
super("error.msg.agent.id.invalid", "Agent with identifier " + id + " does not exist", id);
}
public AgentNotFoundException(final Long id, final EmptyResultDataAccessException e) {
super("error.msg.agent.id.invalid", "Agent with identifier " + id + " does not exist", id, e);
}
public AgentNotFoundException(final String agentCode) {
super("error.msg.agent.code.invalid", "Agent with code " + agentCode + " does not exist", agentCode);
}
}
@@ -0,0 +1,38 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.agent.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when an agent is given a savings account that belongs to somebody else.
*
* An agent's commission is their own earnings, so it is paid into their own account. Accepting
* another person's account number here would route one person's pay into another's balance - a
* mistake that reads as ordinary data entry and would only surface when the person who should have
* been paid asked where their money went.
*/
public class AgentSavingsAccountClientMismatchException extends AbstractPlatformDomainRuleException {
public AgentSavingsAccountClientMismatchException(final String accountNo) {
super("error.msg.dds.agent.savings.account.client.mismatch",
"Savings account " + accountNo + " belongs to a different client. An agent's commission is paid into their own account.",
accountNo);
}
}
@@ -0,0 +1,36 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.agent.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when an agent is given a savings account number that no open account carries.
*
* The agent's commission is paid into this account, so a number that exists only as typed text means
* a payment with nowhere to land - and nothing downstream would catch it, because the commission run
* records what it paid, not whether the account it named was real.
*/
public class AgentSavingsAccountNotFoundException extends AbstractPlatformDomainRuleException {
public AgentSavingsAccountNotFoundException(final String accountNo) {
super("error.msg.dds.agent.savings.account.not.found",
"No open savings account has the number " + accountNo + ". Pick one of the client's own accounts.", accountNo);
}
}
@@ -0,0 +1,41 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.agent.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when an agent's commission account is a fixed or recurring deposit rather than a savings
* account.
*
* Commission is paid in as a credit whenever a commission run posts. A savings account takes credits
* at any time; a fixed deposit is a single sum placed for a term, and a recurring deposit takes only
* its own scheduled instalments - so paying into either is not a thing a bank does, and the posting
* would fail when the run came round. Refusing it as the account is chosen puts the error in front
* of the officer who can fix it, rather than in a batch log weeks later.
*/
public class AgentSavingsAccountNotOperativeException extends AbstractPlatformDomainRuleException {
public AgentSavingsAccountNotOperativeException(final String accountNo, final String accountKind) {
super("error.msg.dds.agent.savings.account.not.operative",
"Account " + accountNo + " is a " + accountKind + " account. Commission has to be paid into an ordinary savings "
+ "account, so pick one of the client's own - not a deposit and not a DDS scheme account.",
accountNo, accountKind);
}
}
@@ -0,0 +1,39 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.commissionslab.data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public final class CommissionSlabData implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private final Long id;
private final String slabType;
private final BigDecimal slabFrom;
private final BigDecimal slabTo;
private final BigDecimal commissionPercent;
}
@@ -0,0 +1,98 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.commissionslab.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
/**
* One row of the agent commission chart: an ACCOUNT slab (commission percentage on collection
* volume) or a COMMISSION slab (the override a senior agent earns on a sub-agent's commission).
* Reference data, not history: a slab says what an agent earns on a given collection volume, and the
* commission run reads whatever the chart says at the time it runs. A mistyped range or percentage
* therefore has to be correctable - the legacy screen offered an edit on every saved row - so a slab
* can be updated. It still cannot be deleted: removing a range silently pays nothing to any agent who
* lands in it, which reads as a system fault rather than a decision.
*/
@Getter
@Entity
@Table(name = "m_dds_commission_slab")
public class CommissionSlab extends AbstractAuditableCustom {
@Column(name = "slab_type", length = 20, nullable = false)
private String slabType;
@Column(name = "slab_from", scale = 6, precision = 19, nullable = false)
private BigDecimal slabFrom;
@Column(name = "slab_to", scale = 6, precision = 19, nullable = false)
private BigDecimal slabTo;
@Column(name = "commission_percent", scale = 6, precision = 19, nullable = false)
private BigDecimal commissionPercent;
protected CommissionSlab() {
//
}
public CommissionSlab(final String slabType, final BigDecimal slabFrom, final BigDecimal slabTo, final BigDecimal commissionPercent) {
this.slabType = slabType;
this.slabFrom = slabFrom;
this.slabTo = slabTo;
this.commissionPercent = commissionPercent;
}
/**
* Corrects this slab, reporting only what actually changed so the audit trail records the
* correction rather than a wholesale rewrite.
*/
public Map<String, Object> update(final JsonCommand command) {
final Map<String, Object> actualChanges = new LinkedHashMap<>(4);
if (command.isChangeInStringParameterNamed("slabType", this.slabType)) {
final String newValue = command.stringValueOfParameterNamed("slabType");
actualChanges.put("slabType", newValue);
this.slabType = newValue;
}
if (command.isChangeInBigDecimalParameterNamed("slabFrom", this.slabFrom)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("slabFrom");
actualChanges.put("slabFrom", newValue);
this.slabFrom = newValue;
}
if (command.isChangeInBigDecimalParameterNamed("slabTo", this.slabTo)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("slabTo");
actualChanges.put("slabTo", newValue);
this.slabTo = newValue;
}
if (command.isChangeInBigDecimalParameterNamed("commissionPercent", this.commissionPercent)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("commissionPercent");
actualChanges.put("commissionPercent", newValue);
this.commissionPercent = newValue;
}
return actualChanges;
}
}
@@ -0,0 +1,31 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.commissionslab.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
/**
* Thrown when a commission slab being corrected no longer exists.
*/
public class CommissionSlabNotFoundException extends AbstractPlatformResourceNotFoundException {
public CommissionSlabNotFoundException(final Long slabId) {
super("error.msg.commissionslab.id.invalid", "Commission slab with identifier " + slabId + " does not exist", slabId);
}
}
@@ -0,0 +1,42 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.commissionslab.exception;
import java.math.BigDecimal;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when a commission slab's range overlaps one that already covers part of it.
*
* The commission run picks the first slab whose range contains the agent's collection volume, so
* two slabs covering the same figure make the rate that gets applied a matter of which row the
* database returns first. The same agent could be paid a different percentage from one month to the
* next with nothing in the data to explain it, which is why this is refused rather than warned
* about.
*/
public class OverlappingCommissionSlabException extends AbstractPlatformDomainRuleException {
public OverlappingCommissionSlabException(final String slabType, final BigDecimal from, final BigDecimal to,
final BigDecimal existingFrom, final BigDecimal existingTo) {
super("error.msg.commissionslab.overlapping",
slabType + " slab " + from.toPlainString() + " to " + to.toPlainString() + " overlaps the existing slab "
+ existingFrom.toPlainString() + " to " + existingTo.toPlainString(),
slabType, from, to, existingFrom, existingTo);
}
}
@@ -27,13 +27,14 @@ import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.command.core.CommandPipeline;
import org.apache.fineract.organisation.monetary.command.CurrencyUpdateCommand;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.organisation.monetary.data.CurrencyConfigurationData;
import org.apache.fineract.organisation.monetary.data.CurrencyUpdateRequest;
import org.apache.fineract.organisation.monetary.data.CurrencyUpdateResponse;
import org.apache.fineract.organisation.monetary.service.OrganisationCurrencyReadPlatformService;
import org.springframework.stereotype.Component;
@@ -44,7 +45,8 @@ import org.springframework.stereotype.Component;
public class CurrenciesApiResource {
private final OrganisationCurrencyReadPlatformService readPlatformService;
private final CommandPipeline commandPipeline;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final DefaultToApiJsonSerializer<CurrencyUpdateRequest> toApiJsonSerializer;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@@ -65,13 +67,12 @@ public class CurrenciesApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update Currency Configuration", description = "Updates the list of currencies permitted for use.")
public CurrencyUpdateResponse updateCurrencies(@Valid CurrencyUpdateRequest request) {
final var command = new CurrencyUpdateCommand();
public String updateCurrencies(@Valid final CurrencyUpdateRequest request) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateCurrency()
.withJson(toApiJsonSerializer.serialize(request)).build();
command.setPayload(request);
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
final Supplier<CurrencyUpdateResponse> response = commandPipeline.send(command);
return response.get();
return toApiJsonSerializer.serialize(result);
}
}
@@ -0,0 +1,63 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.monetary.handler;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.organisation.monetary.data.CurrencyUpdateRequest;
import org.apache.fineract.organisation.monetary.data.CurrencyUpdateResponse;
import org.apache.fineract.organisation.monetary.service.CurrencyWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CURRENCY", action = "UPDATE")
public class UpdateCurrencyCommandHandler implements NewCommandSourceHandler {
private final CurrencyWritePlatformService writePlatformService;
@Autowired
public UpdateCurrencyCommandHandler(final CurrencyWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
final String[] currencyCodes = command.arrayValueOfParameterNamed("currencies");
final List<String> currencies = currencyCodes == null ? List.of() : Arrays.asList(currencyCodes);
final CurrencyUpdateRequest request = CurrencyUpdateRequest.builder().currencies(currencies).build();
final CurrencyUpdateResponse response = this.writePlatformService.updateAllowedCurrencies(request);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.with(Map.of("currencies", response.getCurrencies())) //
.build();
}
}
@@ -24,7 +24,10 @@ public enum PortfolioProductType {
SAVING(2, "productType.saving"), //
CLIENT(5, "productType.client"), //
PROVISIONING(3, "productType.provisioning"), //
SHARES(4, "productType.shares"); //
SHARES(4, "productType.shares"), //
// Current products map their GLs under their own type: sharing SAVING with savings products let a savings
// product and a current product with the same id read each other's GL rows.
CURRENT(6, "productType.current"); //
private final Integer value;
private final String code;
@@ -52,17 +55,25 @@ public enum PortfolioProductType {
return null;
}
// Matched against each constant's own declared value above, not its declaration position --
// those two orderings diverge (CLIENT is declared 3rd but its value is 5), so a switch written
// by position silently mapped every id past SAVING to the wrong type: 3 (really PROVISIONING)
// returned CLIENT, 4 (really SHARES) returned PROVISIONING, 5 (really CLIENT) returned SHARES,
// and 6 (CURRENT) fell through to null -- a live NPE the moment a Current-product GL entry
// (entity_type_enum=6) reached this on the Journal Entries screen.
switch (v) {
case 1:
return LOAN;
case 2:
return SAVING;
case 3:
return CLIENT;
case 4:
return PROVISIONING;
case 5:
case 4:
return SHARES;
case 5:
return CLIENT;
case 6:
return CURRENT;
default:
return null;
}
@@ -78,6 +78,30 @@ public class AmlAlert extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "disposition", length = 20)
private String disposition;
/**
* The other rules this same transaction matched, comma-separated.
*
* <p>
* One transaction raises one alert, under the rule that most warrants attention - otherwise a
* single deposit arrives as several alerts and an officer has to dispose of each, with nothing
* stopping them clearing one while escalating another. The rules that did not win are kept here
* so the case file still shows everything that fired.
*/
@Column(name = "also_matched_rules", length = 500)
private String alsoMatchedRules;
/**
* The worst severity band across every rule this transaction matched — 1 Low, 2 Medium, 3 High.
*
* <p>
* Not the same as the governing rule's own severity. A statutory CTR outranks everything when
* the alert is raised, so a high-severity rule can end up recorded in {@link #alsoMatchedRules}
* and vanish from the screen. An officer disposing of low and medium alerts in a batch would
* then be handed a transaction that had tripped a high-severity rule with nothing to show it.
*/
@Column(name = "highest_severity")
private Integer highestSeverity;
protected AmlAlert() {}
public static AmlAlert raise(AmlTransactionType transactionType, Long transactionId, Client client, AmlTransactionRule rule,
@@ -94,6 +118,58 @@ public class AmlAlert extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return alert;
}
public String getAlsoMatchedRules() {
return this.alsoMatchedRules;
}
public Integer getHighestSeverity() {
return this.highestSeverity;
}
public void recordHighestSeverity(final Integer severity) {
this.highestSeverity = severity;
}
/** Truncated to the column width: the record is a convenience, never a reason to fail a save. */
public void recordAlsoMatchedRules(final String ruleCodes) {
this.alsoMatchedRules = ruleCodes == null || ruleCodes.isBlank() ? null
: (ruleCodes.length() > 500 ? ruleCodes.substring(0, 500) : ruleCodes);
}
/**
* Whether this alert is being worked by somebody other than the given officer.
*
* <p>
* An alert under review belongs to whoever opened it until they decide it or hand it back. A second officer
* reading the same case and reaching their own verdict is the failure this exists to prevent: the transaction
* would carry two reviews, and whichever was written last would be the one on file.
*/
public boolean isHeldByAnother(final AppUser officer) {
return AmlAlertStatus.UNDER_REVIEW.name().equals(this.status) && this.reviewedBy != null && officer != null
&& !this.reviewedBy.getId().equals(officer.getId());
}
/**
* Brings the alert into line with the Principal Officer's conclusion on the case it opened.
*
* <p>
* An escalation is a referral, not a verdict: it says the AML Officer wanted the case looked at. The verdict is
* the Principal Officer's, and when they conclude the case clear the alert has to say so too. Left as ESCALATED,
* the feed would go on showing a transaction as suspicious after the bank had decided it was not - and the officer
* who cleared it would have no way to see their own conclusion reflected where the alert is worked.
*
* <p>
* Distinct from {@link #whitelist}: this does not pass the alert's own immutability guard, because it is not a
* second officer overturning a decision. It is the case this alert was referred into, coming back with its answer.
*/
public void applyStrConclusion(final boolean suspicious, final AppUser principalOfficer, final String notes) {
this.reviewedBy = principalOfficer;
this.reviewedOn = LocalDateTime.now();
this.reviewNotes = notes;
this.disposition = (suspicious ? AmlAlertDisposition.ESCALATE : AmlAlertDisposition.WHITELIST).name();
this.status = (suspicious ? AmlAlertStatus.ESCALATED : AmlAlertStatus.WHITELISTED).name();
}
public void whitelist(AppUser reviewer, String notes) {
this.reviewedBy = reviewer;
this.reviewedOn = LocalDateTime.now();
@@ -44,4 +44,18 @@ public interface AmlAlertRepository extends JpaRepository<AmlAlert, Long> {
* twice, which would put two STR cases in front of the Principal Officer for a single transaction.
*/
List<AmlAlert> findByRule_IdAndTransactionId(Long ruleId, Long transactionId);
/**
* A customer's still-open alerts inside the observation window, oldest first.
*
* <p>
* What a recurring-behaviour review acts on. Open is defined as not yet decided rather than by naming the statuses
* that are open, so a status this query does not know about is still treated as work: an alert wrongly included
* costs a reviewer a glance, one wrongly excluded is a pattern the bank never looked at. A decision about the
* customer's pattern must not reopen alerts an officer has already disposed of individually - and oldest first so
* that a partial failure leaves the longest-standing ones dealt with.
*/
@Query("SELECT a FROM AmlAlert a WHERE a.client.id = :clientId AND a.raisedOn >= :since "
+ "AND a.status NOT IN ('WHITELISTED', 'ESCALATED') ORDER BY a.raisedOn ASC")
List<AmlAlert> findOpenAlertsForClientSince(@Param("clientId") Long clientId, @Param("since") LocalDateTime since);
}
@@ -0,0 +1,163 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import org.apache.fineract.useradministration.domain.AppUser;
/**
* One rule a transaction matched, and what the officer decided about that rule.
*
* <p>
* The alert is the case; this is a finding within it. A single deposit can trip six rules where five are explained by
* the customer's declared business and the sixth is not - one verdict over the whole alert either loses that sixth
* observation or sends the other five to an STR they do not belong in.
*
* <p>
* The rule is held by code rather than by a foreign key to {@link AmlTransactionRule}, and the severity is copied in
* rather than read back through it. An alert records what was detected at the time; re-banding or retiring a rule
* afterwards must not rewrite what the officer was looking at when they decided.
*/
@Entity
@Table(name = "m_aml_alert_rule")
public class AmlAlertRule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "alert_id", nullable = false)
private AmlAlert alert;
@Column(name = "rule_code", nullable = false, length = 100)
private String ruleCode;
@Column(name = "risk_category")
private Integer riskCategory;
@Column(name = "is_governing", nullable = false)
private boolean governing;
@Column(name = "disposition", length = 20)
private String disposition;
@ManyToOne
@JoinColumn(name = "reviewed_by")
private AppUser reviewedBy;
@Column(name = "reviewed_on")
private LocalDateTime reviewedOn;
@Column(name = "review_notes", length = 1000)
private String reviewNotes;
/**
* Whether the officer named this rule, as against it being closed to keep the case consistent.
*
* <p>
* Marking one rule suspicious escalates the transaction and closes every other rule under the same escalation -
* nothing may stay open behind a filed STR. Without this the record would read as though all of them had been
* examined and condemned, and on a transaction that tripped six rules, which one the officer put their name to is
* the substance of the finding.
*/
@Column(name = "directly_decided", nullable = false)
private boolean directlyDecided;
protected AmlAlertRule() {}
public static AmlAlertRule of(final AmlAlert alert, final String ruleCode, final Integer riskCategory,
final boolean governing) {
final AmlAlertRule finding = new AmlAlertRule();
finding.alert = alert;
finding.ruleCode = ruleCode;
finding.riskCategory = riskCategory;
finding.governing = governing;
return finding;
}
/**
* @param directlyDecided
* true when the officer named this rule; false when it was closed alongside one they named
*/
public void decide(final AmlAlertDisposition disposition, final AppUser reviewer, final String notes,
final boolean directlyDecided) {
this.disposition = disposition.name();
this.reviewedBy = reviewer;
this.reviewedOn = LocalDateTime.now();
this.reviewNotes = notes;
this.directlyDecided = directlyDecided;
}
public boolean isDirectlyDecided() {
return this.directlyDecided;
}
public boolean isDecided() {
return this.disposition != null;
}
public boolean isEscalated() {
return AmlAlertDisposition.ESCALATE.name().equals(this.disposition);
}
public Long getId() {
return this.id;
}
public AmlAlert getAlert() {
return this.alert;
}
public String getRuleCode() {
return this.ruleCode;
}
public Integer getRiskCategory() {
return this.riskCategory;
}
public boolean isGoverning() {
return this.governing;
}
public String getDisposition() {
return this.disposition;
}
public AppUser getReviewedBy() {
return this.reviewedBy;
}
public LocalDateTime getReviewedOn() {
return this.reviewedOn;
}
public String getReviewNotes() {
return this.reviewNotes;
}
}
@@ -0,0 +1,31 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
import java.util.Collection;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AmlAlertRuleRepository extends JpaRepository<AmlAlertRule, Long> {
List<AmlAlertRule> findByAlertId(Long alertId);
/** Every finding across a page of alerts, in one read rather than one query per alert. */
List<AmlAlertRule> findByAlertIdIn(Collection<Long> alertIds);
}
@@ -0,0 +1,106 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
/**
* How far a customer's repetition has gone, and what the bank's answer to it has to be.
*
* <p>
* A customer who surfaces once is a question; one who surfaces for the fortieth time is an answer the bank has not
* acted on. Without a ladder every review looked the same - same screen, same three buttons, same outcome - so the
* fortieth review cost an officer the same effort as the first and changed as little.
*
* <p>
* Each tier does two things: it sets how long a review holds the customer out of the queue, and it says what the
* reviewer is expected to do. The windows shorten as the tier rises, because a customer the bank is more worried
* about should come back sooner, not later.
*/
public enum AmlRecurringEscalationTier {
/** Ordinary repetition. A review stands for a quarter unless the count climbs into the next tier. */
ROUTINE(0, 90),
/**
* Enough repetition that the customer's risk rating is no longer just a KYC-time judgement. Reviewed monthly, and
* the reviewer is asked to consider whether the rating still fits.
*/
ELEVATED(7, 30),
/**
* Beyond what a branch officer should be closing on their own. Reviewed fortnightly, and the review is expected
* to be taken by someone senior.
*/
SENIOR(15, 14),
/**
* Repetition at a level that is itself the suspicion. An STR is the expected outcome, and clearing at this tier
* is a decision that has to be defensible on its own - so the review stands for a week only.
*/
MANDATORY_STR(30, 7);
private final int minimumAlertCount;
private final int reviewValidityDays;
AmlRecurringEscalationTier(final int minimumAlertCount, final int reviewValidityDays) {
this.minimumAlertCount = minimumAlertCount;
this.reviewValidityDays = reviewValidityDays;
}
/** The tier a customer sitting on this many alerts falls into. */
public static AmlRecurringEscalationTier forAlertCount(final int alertCount) {
AmlRecurringEscalationTier tier = ROUTINE;
for (final AmlRecurringEscalationTier candidate : values()) {
if (alertCount >= candidate.minimumAlertCount) {
tier = candidate;
}
}
return tier;
}
public int getMinimumAlertCount() {
return minimumAlertCount;
}
public int getReviewValidityDays() {
return reviewValidityDays;
}
/**
* How far the alert count may climb before a standing review is set aside and the customer returns early.
*
* <p>
* A validity window alone would hold a customer out while their behaviour changed underneath it: reviewed at four
* alerts and then trips forty more the same week, the review would still stand for the full quarter. Allowing the
* count to grow by the tier's own span before forcing a return keeps ordinary repetition quiet while letting a
* genuine change in behaviour break through. Tighter at the higher tiers, where less change should be tolerated.
*/
public int alertGrowthBeforeEarlyReturn() {
return switch (this) {
case ROUTINE -> 5;
case ELEVATED -> 4;
case SENIOR -> 3;
case MANDATORY_STR -> 2;
};
}
/** Whether a review at this tier is expected to raise an STR rather than close the customer off. */
public boolean expectsStr() {
return this == MANDATORY_STR;
}
}
@@ -26,6 +26,7 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDate;
import java.time.LocalDateTime;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.useradministration.domain.AppUser;
@@ -65,6 +66,20 @@ public class AmlRecurringReview {
@Column(name = "reviewed_on", nullable = false)
private LocalDateTime reviewedOn;
/**
* The date this review stops standing. Null on reviews taken before validity existed, which keep the older
* behaviour of lapsing the moment one more alert lands.
*/
@Column(name = "valid_until")
private LocalDate validUntil;
@Column(name = "escalation_tier", length = 20)
private String escalationTier;
/** How many of the customer's open alerts this review actually disposed of. */
@Column(name = "alerts_actioned")
private Integer alertsActioned;
protected AmlRecurringReview() {}
public static AmlRecurringReview record(final Client client, final int alertCountAtReview,
@@ -76,9 +91,22 @@ public class AmlRecurringReview {
review.notes = notes;
review.reviewedBy = reviewedBy;
review.reviewedOn = LocalDateTime.now();
// The tier follows from the count, not from what the reviewer picked: how often a customer has surfaced is a
// fact about them, and letting the outcome set it would let a customer be held at ROUTINE indefinitely by
// always being cleared.
final AmlRecurringEscalationTier tier = AmlRecurringEscalationTier.forAlertCount(alertCountAtReview);
review.escalationTier = tier.name();
review.validUntil = LocalDate.now().plusDays(tier.getReviewValidityDays());
review.alertsActioned = 0;
return review;
}
/** Records how many open alerts the review disposed of, once the caller has acted on them. */
public void recordAlertsActioned(final int count) {
this.alertsActioned = count;
}
public Long getId() {
return id;
}
@@ -106,4 +134,35 @@ public class AmlRecurringReview {
public LocalDateTime getReviewedOn() {
return reviewedOn;
}
public LocalDate getValidUntil() {
return validUntil;
}
public AmlRecurringEscalationTier getEscalationTier() {
return escalationTier == null ? AmlRecurringEscalationTier.ROUTINE : AmlRecurringEscalationTier.valueOf(escalationTier);
}
public Integer getAlertsActioned() {
return alertsActioned;
}
/**
* Whether this review still holds the customer out of the review queue.
*
* <p>
* Two ways it stops standing. Its window runs out, or the customer's behaviour moves on far enough that the
* review no longer describes them - judged by how much the alert count has grown since, against the allowance for
* the tier it was taken at. A review with no window at all is a pre-existing one, and keeps the original rule of
* lapsing as soon as one more alert arrives.
*/
public boolean stillStands(final int currentAlertCount) {
if (validUntil == null) {
return currentAlertCount <= alertCountAtReview;
}
if (LocalDate.now().isAfter(validUntil)) {
return false;
}
return currentAlertCount - alertCountAtReview < getEscalationTier().alertGrowthBeforeEarlyReturn();
}
}
@@ -0,0 +1,83 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
/**
* One alert covered by an STR case.
*
* <p>
* A case escalated from a single transaction covers that one alert. A case raised off a customer's repeated behaviour
* covers all the alerts that make up the pattern - which is what an STR to FIU-IND actually describes, and what the
* Principal Officer has to be able to read in one place.
*/
@Entity
@Table(name = "m_aml_str_case_alert")
public class AmlStrCaseAlert {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(optional = false)
@JoinColumn(name = "str_case_id", nullable = false)
private AmlStrCase strCase;
@ManyToOne(optional = false)
@JoinColumn(name = "alert_id", nullable = false)
private AmlAlert alert;
@Column(name = "added_on_utc", nullable = false)
private LocalDateTime addedOn;
protected AmlStrCaseAlert() {}
public static AmlStrCaseAlert link(final AmlStrCase strCase, final AmlAlert alert) {
final AmlStrCaseAlert link = new AmlStrCaseAlert();
link.strCase = strCase;
link.alert = alert;
link.addedOn = LocalDateTime.now();
return link;
}
public Long getId() {
return id;
}
public AmlStrCase getStrCase() {
return strCase;
}
public AmlAlert getAlert() {
return alert;
}
public LocalDateTime getAddedOn() {
return addedOn;
}
}
@@ -0,0 +1,40 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AmlStrCaseAlertRepository extends JpaRepository<AmlStrCaseAlert, Long> {
/** The alerts one case covers, oldest first - the order the pattern happened in. */
List<AmlStrCaseAlert> findByStrCase_IdOrderByAlert_RaisedOnAsc(Long strCaseId);
/**
* The case already covering an alert, if any.
*
* <p>
* What stops a transaction reaching FIU-IND twice: an alert already inside a case is not attached to a second
* one, however it comes to be escalated again.
*/
Optional<AmlStrCaseAlert> findByAlert_Id(Long alertId);
long countByStrCase_Id(Long strCaseId);
}
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.aml.domain;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@@ -28,6 +29,9 @@ public interface AmlStrCaseRepository extends JpaRepository<AmlStrCase, Long> {
long countByClient_Id(Long clientId);
/** The case already open for a transaction, so escalating it again does not open a second one. */
Optional<AmlStrCase> findByAlertId(Long alertId);
List<AmlStrCase> findByStatus(String status);
@Query("SELECT c FROM AmlStrCase c WHERE c.status = 'CONCLUDED_SUSPICIOUS' AND c.concludedOn BETWEEN :from AND :to")
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.aml.domain;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -32,4 +33,10 @@ public interface AmlTransactionRuleRepository extends JpaRepository<AmlTransacti
* by hand from the ledger.
*/
Optional<AmlTransactionRule> findByRuleCode(String ruleCode);
/**
* Every rule named in the given codes, in one read. The alert screen resolves the rules each transaction matched
* into descriptions and severities; doing that a code at a time would be one query per rule per alert.
*/
List<AmlTransactionRule> findByRuleCodeIn(Collection<String> ruleCodes);
}
@@ -0,0 +1,219 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* What one customer's money actually did over a period, and what that says about their rating.
*
* <p>
* A risk rating describes behaviour, and behaviour is a period rather than an event. Judging it from the alert feed
* meant a customer appeared the moment one transaction tripped a rule and was recommended for re-rating on that alone
* - a single shop purchase could argue for lowering a High rating, and a single large deposit for raising a Low one.
* Neither is a pattern.
*
* <p>
* One row is one window for one customer, computed nightly. Upgrades and downgrades read different windows, because
* the questions are not symmetric: a concern is about what the customer is doing now, and confidence needs a year of
* it. The suggestion is stored with the figures it was drawn from, so an officer reviewing it months later sees what
* the system was looking at rather than a conclusion with no working.
*/
@Entity
@Table(name = "m_client_behaviour_window")
public class ClientBehaviourWindow {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "client_id", nullable = false)
private Long clientId;
@Column(name = "window_from", nullable = false)
private LocalDate windowFrom;
@Column(name = "window_to", nullable = false)
private LocalDate windowTo;
@Column(name = "txn_count")
private Integer txnCount;
@Column(name = "distinct_months")
private Integer distinctMonths;
@Column(name = "credit_total", precision = 19, scale = 6)
private BigDecimal creditTotal;
@Column(name = "debit_total", precision = 19, scale = 6)
private BigDecimal debitTotal;
@Column(name = "largest_txn", precision = 19, scale = 6)
private BigDecimal largestTxn;
@Column(name = "cash_txn_count")
private Integer cashTxnCount;
@Column(name = "cash_total", precision = 19, scale = 6)
private BigDecimal cashTotal;
@Column(name = "large_txn_count")
private Integer largeTxnCount;
@Column(name = "declared_annual_income", precision = 19, scale = 6)
private BigDecimal declaredAnnualIncome;
@Column(name = "declared_annual_turnover", precision = 19, scale = 6)
private BigDecimal declaredAnnualTurnover;
@Column(name = "income_ratio", precision = 19, scale = 6)
private BigDecimal incomeRatio;
@Column(name = "alert_count")
private Integer alertCount;
@Column(name = "str_case_count")
private Integer strCaseCount;
@Column(name = "risk_category_at_review")
private Integer riskCategoryAtReview;
/** UP, DOWN or HOLD. */
@Column(name = "suggested_direction", length = 10)
private String suggestedDirection;
@Column(name = "suggested_category")
private Integer suggestedCategory;
@Column(name = "suggestion_reason", length = 500)
private String suggestionReason;
@Column(name = "computed_on")
private LocalDateTime computedOn;
protected ClientBehaviourWindow() {}
public static ClientBehaviourWindow of(final Long clientId, final LocalDate windowFrom, final LocalDate windowTo) {
final ClientBehaviourWindow window = new ClientBehaviourWindow();
window.clientId = clientId;
window.windowFrom = windowFrom;
window.windowTo = windowTo;
window.computedOn = LocalDateTime.now();
return window;
}
public void recordDistinctMonths(final Integer months) {
this.distinctMonths = months;
}
public Integer getDistinctMonths() {
return this.distinctMonths;
}
public void recordActivity(final Integer txnCount, final BigDecimal creditTotal, final BigDecimal debitTotal,
final BigDecimal largestTxn, final Integer cashTxnCount, final BigDecimal cashTotal, final Integer largeTxnCount) {
this.txnCount = txnCount;
this.creditTotal = creditTotal;
this.debitTotal = debitTotal;
this.largestTxn = largestTxn;
this.cashTxnCount = cashTxnCount;
this.cashTotal = cashTotal;
this.largeTxnCount = largeTxnCount;
}
public void recordMonitoring(final Integer alertCount, final Integer strCaseCount, final Integer riskCategoryAtReview) {
this.alertCount = alertCount;
this.strCaseCount = strCaseCount;
this.riskCategoryAtReview = riskCategoryAtReview;
}
/**
* Turnover measured against what the customer said to expect.
*
* <p>
* Null rather than zero when no income was declared: the ratio is unknown, not nil, and a rule that treats an
* unknown as a benign zero would clear every customer the bank never asked.
*/
public void recordDeclared(final BigDecimal income, final BigDecimal turnover, final BigDecimal periodTurnover) {
this.declaredAnnualIncome = income;
this.declaredAnnualTurnover = turnover;
final BigDecimal basis = income != null && income.signum() > 0 ? income
: (turnover != null && turnover.signum() > 0 ? turnover : null);
this.incomeRatio = basis == null || periodTurnover == null ? null
: periodTurnover.divide(basis, 6, java.math.RoundingMode.HALF_UP);
}
public void recordSuggestion(final String direction, final Integer category, final String reason) {
this.suggestedDirection = direction;
this.suggestedCategory = category;
this.suggestionReason = reason == null || reason.length() <= 500 ? reason : reason.substring(0, 500);
}
public Long getClientId() {
return this.clientId;
}
public LocalDate getWindowFrom() {
return this.windowFrom;
}
public LocalDate getWindowTo() {
return this.windowTo;
}
public Integer getTxnCount() {
return this.txnCount;
}
public BigDecimal getCashTotal() {
return this.cashTotal;
}
public Integer getLargeTxnCount() {
return this.largeTxnCount;
}
public Integer getAlertCount() {
return this.alertCount;
}
public Integer getStrCaseCount() {
return this.strCaseCount;
}
public Integer getRiskCategoryAtReview() {
return this.riskCategoryAtReview;
}
public String getSuggestedDirection() {
return this.suggestedDirection;
}
public Integer getSuggestedCategory() {
return this.suggestedCategory;
}
}
@@ -0,0 +1,43 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.domain;
import java.time.LocalDate;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface ClientBehaviourWindowRepository extends JpaRepository<ClientBehaviourWindow, Long> {
List<ClientBehaviourWindow> findByClientId(Long clientId);
/**
* Clears the windows computed for a given day before the job writes that day's.
*
* <p>
* The job is a recomputation, not an append. Re-running it - after a failure, or twice in a night - must leave one
* set of windows rather than a customer appearing several times on the review screen with slightly different
* figures.
*/
@Modifying
@Query("DELETE FROM ClientBehaviourWindow w WHERE w.windowTo = :windowTo")
void deleteByWindowTo(@Param("windowTo") LocalDate windowTo);
}
@@ -291,6 +291,31 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "form15_fy", length = 10)
private String form15Fy;
// ── Risk profile ─────────────────────────────────────────────────────────
/**
* Customer facts the risk re-rating policy tests against, as against anything the customer transacted.
*
* <p>
* A politically exposed person is not downgraded because their activity looks ordinary — that is precisely the
* risk — and the same holds for a sanctions or adverse-media match. False means "not recorded as such", which is
* the correct reading for a book where nobody was ever asked; it is not an assertion that the customer is clean.
*/
@Column(name = "is_pep", nullable = false)
private boolean isPep;
@Column(name = "sanctions_match", nullable = false)
private boolean sanctionsMatch;
@Column(name = "adverse_media_match", nullable = false)
private boolean adverseMediaMatch;
/** What the customer told the bank to expect, which their actual turnover is measured against. */
@Column(name = "declared_annual_income", precision = 19, scale = 6)
private BigDecimal declaredAnnualIncome;
@Column(name = "declared_annual_turnover", precision = 19, scale = 6)
private BigDecimal declaredAnnualTurnover;
// ── CKYCRR ───────────────────────────────────────────────────────────────
/** Central KYC Records Registry number issued by CERSAI */
@Column(name = "ckyc_number", length = 14)
@@ -527,6 +552,19 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
v -> this.aadhaarReceiptRemarks = v);
updateDateField(command, "aadhaarReceiptTakenOn", this.aadhaarReceiptTakenOn, actualChanges,
v -> this.aadhaarReceiptTakenOn = v);
// Risk profile. These are determinations about the customer rather than about a transaction, and the
// re-rating policy reads them directly: a PEP or a sanctions match holds a rating at High however ordinary the
// period's activity looks.
updateBooleanField(command, "isPep", this.isPep, actualChanges, v -> this.isPep = v != null ? v : false);
updateBooleanField(command, "sanctionsMatch", this.sanctionsMatch, actualChanges,
v -> this.sanctionsMatch = v != null ? v : false);
updateBooleanField(command, "adverseMediaMatch", this.adverseMediaMatch, actualChanges,
v -> this.adverseMediaMatch = v != null ? v : false);
updateBigDecimalField(command, "declaredAnnualIncome", this.declaredAnnualIncome, actualChanges,
v -> this.declaredAnnualIncome = v);
updateBigDecimalField(command, "declaredAnnualTurnover", this.declaredAnnualTurnover, actualChanges,
v -> this.declaredAnnualTurnover = v);
updateStringField(command, "enterprisePan", this.enterprisePan, actualChanges, v -> this.enterprisePan = v);
updateStringField(command, "cinNumber", this.cinNumber, actualChanges, v -> this.cinNumber = v);
updateStringField(command, "dinNumber", this.dinNumber, actualChanges, v -> this.dinNumber = v);
@@ -898,6 +936,26 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
recalculateExpiryForRiskCategory();
}
public boolean isPep() {
return this.isPep;
}
public boolean isSanctionsMatch() {
return this.sanctionsMatch;
}
public boolean isAdverseMediaMatch() {
return this.adverseMediaMatch;
}
public BigDecimal getDeclaredAnnualIncome() {
return this.declaredAnnualIncome;
}
public BigDecimal getDeclaredAnnualTurnover() {
return this.declaredAnnualTurnover;
}
public String getPanNumber() {
return panNumber;
}
@@ -0,0 +1,74 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* One number the risk re-rating policy is made of - a window length, a minimum transaction count, a cash threshold.
*
* <p>
* Held as data because these are the bank's risk appetite rather than engineering constants. How long a customer must
* behave before a High rating comes down is a compliance judgement, and a co-operative bank whose members transact
* three times a month needs different figures from one lending to traders.
*/
@Entity
@Table(name = "m_risk_policy_parameter")
public class RiskPolicyParameter {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "parameter_key", nullable = false, length = 80)
private String parameterKey;
@Column(name = "parameter_value", nullable = false, precision = 19, scale = 2)
private BigDecimal parameterValue;
@Column(name = "description", length = 500)
private String description;
@Column(name = "last_modified_by")
private Long lastModifiedBy;
@Column(name = "last_modified_on")
private LocalDateTime lastModifiedOn;
protected RiskPolicyParameter() {}
public String getParameterKey() {
return this.parameterKey;
}
public BigDecimal getParameterValue() {
return this.parameterValue;
}
public String getDescription() {
return this.description;
}
}
@@ -0,0 +1,23 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface RiskPolicyParameterRepository extends JpaRepository<RiskPolicyParameter, Long> {}
@@ -0,0 +1,61 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.accounttype.data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Builder;
import lombok.Getter;
/**
* Immutable data object representing a DDS Account Type (the legacy Account Type Creation record).
*/
@Getter
@Builder(toBuilder = true)
public final class DdsAccountTypeData implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private final Long id;
private final String accountTypeGroup;
private final String accountTypeCode;
private final String accountTypeDescription;
private final Boolean autoInterestPost;
private final String autoInterestPostWhen;
private final Integer autoInterestPostDay;
/** Whether an account on this scheme closes itself on its maturity date. */
private final Boolean maturityAutoClose;
/**
* What this scheme charges for closing an account before its term is up: how long it may not be
* closed at all, what comes off the customer's rate if they bear the cost, and what comes off the
* agent's commission either way. Read at the moment of closure, so a change on the scheme takes
* effect on accounts already open.
*/
private final Integer prematureLockinMonths;
private final BigDecimal prematureInterestReduction;
private final BigDecimal prematureAgentCommissionPenalty;
/** The scheme's annual interest rate, which an account opened on it takes. */
private final BigDecimal interestRate;
private final Boolean active;
private final LocalDateTime createdDate;
private final LocalDateTime lastModifiedDate;
}
@@ -0,0 +1,267 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.accounttype.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
/**
* A DDS scheme variant, as registered on the legacy "Account Type Creation" screen: a group (e.g.
* "DDS"), a short code (e.g. "DS"), a description (e.g. "DAILY DEPOSIT"), and whether interest is
* posted automatically and how often.
*
* This is master/reference data, not a deposit product - a Party picks one of these by code in its
* "Sub Type" field. Nothing here drives interest calculation: the DDS batch job accrues interest
* straight off each Party's own rate and balance.
*/
@Getter
@Entity
@Table(name = "m_dds_account_type", uniqueConstraints = {
@UniqueConstraint(columnNames = { "account_type_code" }, name = "uq_dds_account_type_code") })
public class DdsAccountType extends AbstractAuditableCustom {
@Column(name = "account_type_group", length = 60, nullable = false)
private String accountTypeGroup;
@Column(name = "account_type_code", length = 20, nullable = false)
private String accountTypeCode;
@Column(name = "account_type_description", length = 200, nullable = false)
private String accountTypeDescription;
@Column(name = "auto_interest_post", nullable = false)
private boolean autoInterestPost;
@Column(name = "auto_interest_post_when", length = 20)
private String autoInterestPostWhen;
/** Day of the month interest posts on. Only meaningful when {@code autoInterestPost} is set. */
@Column(name = "auto_interest_post_day")
private Integer autoInterestPostDay;
/**
* Whether an account on this scheme closes itself the day it matures, instead of waiting for a
* clerk to pay it out by hand.
*
* Kept on the scheme rather than switched on bank-wide, for the same reason interest posting is:
* a bank runs several schemes at once and will not want them all behaving the same way. It says
* only whether the closing should happen, never when - the batch run decides that, exactly as it
* does for {@code autoInterestPost}.
*
* A scheme with this set still only closes an account that has somewhere for the money to go. If
* the account names no savings account to be paid into, nothing happens automatically and the
* clerk pays it out as before: money is never sent somewhere nobody chose.
*/
@Column(name = "maturity_auto_close", nullable = false)
private boolean maturityAutoClose;
/**
* How long an account on this scheme must run before it may be closed early at all. Below this
* the request is refused outright rather than charged for.
*
* Per-scheme rather than one bank-wide figure: a lock-in longer than the scheme's own term would
* make early closure unreachable, so a two-month scheme and a five-year one cannot share one.
*/
@Column(name = "premature_lockin_months", nullable = false)
private Integer prematureLockinMonths;
/**
* Percentage points off the customer's interest rate when the customer bears the cost of closing
* early - a 2% scheme pays 1% over the whole period, not only the part left to run.
*/
@Column(name = "premature_interest_reduction", scale = 6, precision = 19, nullable = false)
private BigDecimal prematureInterestReduction;
/**
* Percentage off the collecting agent's commission on an early closure, applied whichever way the
* shortfall is met - an account closed early is a round the agent did not keep going.
*/
@Column(name = "premature_agent_commission_penalty", scale = 6, precision = 19, nullable = false)
private BigDecimal prematureAgentCommissionPenalty;
/**
* The scheme's annual interest rate. Mandatory: an account opened on the scheme takes this rate,
* rather than one typed at the counter for each customer.
*/
@Column(name = "interest_rate", scale = 6, precision = 19, nullable = false)
private BigDecimal interestRate;
/**
* An account type is retired by deactivating it, never by deleting: Parties are opened against
* it, so removing the row would orphan every account already using it. Inactive types stay out
* of the pick lists for new accounts but leave existing accounts untouched.
*/
@Column(name = "is_active", nullable = false)
private boolean active;
protected DdsAccountType() {
//
}
private DdsAccountType(final JsonCommand command) {
this.accountTypeGroup = command.stringValueOfParameterNamed("accountTypeGroup");
this.accountTypeCode = normaliseCode(command.stringValueOfParameterNamed("accountTypeCode"));
this.accountTypeDescription = command.stringValueOfParameterNamed("accountTypeDescription");
this.autoInterestPost = command.booleanPrimitiveValueOfParameterNamed("autoInterestPost");
this.autoInterestPostWhen = StringUtils
.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("autoInterestPostWhen"), null);
this.autoInterestPostDay = this.autoInterestPost
? command.integerValueOfParameterNamedDefaultToNullIfZero("autoInterestPostDay")
: null;
this.maturityAutoClose = command.booleanPrimitiveValueOfParameterNamed("maturityAutoClose");
this.prematureLockinMonths = valueOr(command, "prematureLockinMonths", DEFAULT_LOCKIN_MONTHS);
this.prematureInterestReduction = valueOr(command, "prematureInterestReduction", DEFAULT_INTEREST_REDUCTION);
this.prematureAgentCommissionPenalty = valueOr(command, "prematureAgentCommissionPenalty", DEFAULT_AGENT_PENALTY);
this.interestRate = command.bigDecimalValueOfParameterNamed("interestRate");
// A newly created account type is available for use straight away.
this.active = !command.parameterExists("active") || command.booleanPrimitiveValueOfParameterNamed("active");
}
/**
* The bank's stated terms, used when a scheme is created without saying otherwise. They are
* defaults rather than fixed rules - the screen offers them and the bank changes them per scheme.
*/
private static final int DEFAULT_LOCKIN_MONTHS = 12;
private static final BigDecimal DEFAULT_INTEREST_REDUCTION = BigDecimal.ONE;
private static final BigDecimal DEFAULT_AGENT_PENALTY = BigDecimal.ONE;
private static Integer valueOr(final JsonCommand command, final String parameter, final int fallback) {
final Integer supplied = command.parameterExists(parameter)
? command.integerValueOfParameterNamedDefaultToNullIfZero(parameter)
: null;
return supplied == null ? fallback : supplied;
}
private static BigDecimal valueOr(final JsonCommand command, final String parameter, final BigDecimal fallback) {
// Deliberately not "default to null if zero": zero is a real answer here - a scheme that
// charges the customer nothing for closing early - and must not be read as "unset".
final BigDecimal supplied = command.parameterExists(parameter)
? command.bigDecimalValueOfParameterNamed(parameter)
: null;
return supplied == null ? fallback : supplied;
}
/**
* Codes are the account type's identity and are matched against elsewhere, so "ds" and "DS" must
* not become two different types - they are stored upper-cased and trimmed.
*/
private static String normaliseCode(final String code) {
return code == null ? null : code.trim().toUpperCase();
}
public static DdsAccountType fromJson(final JsonCommand command) {
return new DdsAccountType(command);
}
public Map<String, Object> update(final JsonCommand command) {
final Map<String, Object> actualChanges = new LinkedHashMap<>(6);
if (command.isChangeInStringParameterNamed("accountTypeGroup", this.accountTypeGroup)) {
final String newValue = command.stringValueOfParameterNamed("accountTypeGroup");
actualChanges.put("accountTypeGroup", newValue);
this.accountTypeGroup = newValue;
}
final String incomingCode = normaliseCode(command.stringValueOfParameterNamedAllowingNull("accountTypeCode"));
if (incomingCode != null && !incomingCode.equals(this.accountTypeCode)) {
actualChanges.put("accountTypeCode", incomingCode);
this.accountTypeCode = incomingCode;
}
if (command.isChangeInStringParameterNamed("accountTypeDescription", this.accountTypeDescription)) {
final String newValue = command.stringValueOfParameterNamed("accountTypeDescription");
actualChanges.put("accountTypeDescription", newValue);
this.accountTypeDescription = newValue;
}
if (command.isChangeInBooleanParameterNamed("autoInterestPost", this.autoInterestPost)) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("autoInterestPost");
actualChanges.put("autoInterestPost", newValue);
this.autoInterestPost = newValue;
}
if (command.isChangeInStringParameterNamed("autoInterestPostWhen", this.autoInterestPostWhen)) {
final String newValue = command.stringValueOfParameterNamed("autoInterestPostWhen");
actualChanges.put("autoInterestPostWhen", newValue);
this.autoInterestPostWhen = StringUtils.defaultIfEmpty(newValue, null);
}
if (command.isChangeInIntegerParameterNamed("autoInterestPostDay", this.autoInterestPostDay)) {
final Integer newValue = command.integerValueOfParameterNamedDefaultToNullIfZero("autoInterestPostDay");
actualChanges.put("autoInterestPostDay", newValue);
this.autoInterestPostDay = newValue;
}
if (command.isChangeInBooleanParameterNamed("maturityAutoClose", this.maturityAutoClose)) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("maturityAutoClose");
actualChanges.put("maturityAutoClose", newValue);
this.maturityAutoClose = newValue;
}
// A posting day only means anything while auto-posting is on.
if (!this.autoInterestPost) {
this.autoInterestPostDay = null;
}
if (command.isChangeInIntegerParameterNamed("prematureLockinMonths", this.prematureLockinMonths)) {
final Integer newValue = command.integerValueOfParameterNamedDefaultToNullIfZero("prematureLockinMonths");
actualChanges.put("prematureLockinMonths", newValue);
// Zero lock-in is meaningful - it lets an account be closed early from day one - but the
// column cannot hold null, so it is kept as zero rather than reset to the default.
this.prematureLockinMonths = newValue == null ? 0 : newValue;
}
if (command.isChangeInBigDecimalParameterNamed("prematureInterestReduction", this.prematureInterestReduction)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("prematureInterestReduction");
actualChanges.put("prematureInterestReduction", newValue);
this.prematureInterestReduction = newValue == null ? BigDecimal.ZERO : newValue;
}
if (command.isChangeInBigDecimalParameterNamed("prematureAgentCommissionPenalty", this.prematureAgentCommissionPenalty)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("prematureAgentCommissionPenalty");
actualChanges.put("prematureAgentCommissionPenalty", newValue);
this.prematureAgentCommissionPenalty = newValue == null ? BigDecimal.ZERO : newValue;
}
// The scheme's rate - every open account on it follows it (see the account type service).
if (command.parameterExists("interestRate") && command.isChangeInBigDecimalParameterNamed("interestRate", this.interestRate)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("interestRate");
actualChanges.put("interestRate", newValue);
this.interestRate = newValue;
}
if (command.isChangeInBooleanParameterNamed("active", this.active)) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("active");
actualChanges.put("active", newValue);
this.active = newValue;
}
return actualChanges;
}
}
@@ -0,0 +1,36 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.accounttype.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException;
/**
* A {@link RuntimeException} thrown when a DDS account type resource is not found.
*/
public class DdsAccountTypeNotFoundException extends AbstractPlatformResourceNotFoundException {
public DdsAccountTypeNotFoundException(final Long id) {
super("error.msg.dds.account.type.id.invalid", "DDS account type with identifier " + id + " does not exist", id);
}
public DdsAccountTypeNotFoundException(final Long id, final EmptyResultDataAccessException e) {
super("error.msg.dds.account.type.id.invalid", "DDS account type with identifier " + id + " does not exist", id, e);
}
}
@@ -0,0 +1,58 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.batch.data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder(toBuilder = true)
public final class BatchRunData implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private final Long id;
private final String jobType;
private final String branchScope;
/** Which DDS scheme the run covered, or null for every scheme. Set by the automatic posting job. */
private final String subType;
private final LocalDate jobDate;
private final Long agentFrom;
private final Long agentTo;
private final Long partyFrom;
private final Long partyTo;
private final String status;
private final Integer totalRows;
/** How many rows a crediting run has applied. Equal to totalRows once it has finished. */
private final Integer rowsPosted;
/** Why a run stopped, when it did - a crediting run happens in the background and has no other voice. */
private final String failureReason;
private final BigDecimal totalAmount;
private final LocalDate generatedOnDate;
private final LocalDate postedOnDate;
private final Collection<BatchRunRowData> rows;
}
@@ -0,0 +1,57 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.batch.data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public final class BatchRunRowData implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private final Long id;
private final Long partyId;
private final String partyCode;
private final String partyName;
private final Long agentId;
private final String agentCode;
private final String agentName;
/** What the row applies: interest, or commission net of every deduction. */
private final BigDecimal amount;
/**
* How that amount was arrived at. Null on rows where it does not apply - a posting row moves a
* figure an earlier calculation run worked out, so the rate and base belong to that run.
*/
private final BigDecimal baseAmount;
private final BigDecimal ratePercent;
private final BigDecimal grossAmount;
private final BigDecimal tdsAmount;
private final BigDecimal deductionAmount;
private final LocalDate fromDate;
private final LocalDate toDate;
}
@@ -0,0 +1,180 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.batch.domain;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderBy;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
/**
* One Calculate-then-Post batch run - the deliberate two-step control from the legacy Interest Part
* menu (Interest Calculation -> Interest Posting, OD Interest Calculation -> OD Interest Posting,
* Commission Calculation -> Commission Posting): a run is generated (rows computed and saved as a
* preview, nothing else changes), then posted (a separate, explicit step that applies exactly the
* rows that were previewed - never a fresh recalculation, so what gets applied is always what was
* seen).
*/
@Getter
@Entity
@Table(name = "m_dds_batch_run")
public class BatchRun extends AbstractAuditableCustom {
@Column(name = "job_type", length = 40, nullable = false)
private String jobType;
@Column(name = "branch_scope", length = 20, nullable = false)
private String branchScope;
/**
* Which DDS scheme this run covered, or null for every scheme.
*
* A posting day belongs to an account type rather than to the bank, so a run started by the
* scheduled job covers one scheme at a time - otherwise the scheme due on the 15th would drag the
* month-end scheme's accounts along with it. A run started by hand from the screen leaves this
* null and covers everything, as it always did.
*/
@Column(name = "sub_type", length = 60)
private String subType;
@Column(name = "job_date", nullable = false)
private LocalDate jobDate;
@Column(name = "agent_from", nullable = false)
private Long agentFrom;
@Column(name = "agent_to", nullable = false)
private Long agentTo;
@Column(name = "party_from")
private Long partyFrom;
@Column(name = "party_to")
private Long partyTo;
@Column(name = "status", length = 20, nullable = false)
private String status;
@Column(name = "total_rows", nullable = false)
private Integer totalRows;
/** How many of those rows have been credited so far - what a screen reads to show progress. */
@Column(name = "rows_posted", nullable = false)
private Integer rowsPosted = 0;
/** Why a run stopped, when it did. A background run has nowhere else to say so. */
@Column(name = "failure_reason", length = 500)
private String failureReason;
@Column(name = "total_amount", scale = 6, precision = 19, nullable = false)
private BigDecimal totalAmount;
@Column(name = "generated_on_date", nullable = false)
private LocalDate generatedOnDate;
@Column(name = "posted_on_date")
private LocalDate postedOnDate;
@OneToMany(mappedBy = "batchRun", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@OrderBy("id")
private List<BatchRunRow> rows = new ArrayList<>();
protected BatchRun() {
//
}
public BatchRun(final String jobType, final String branchScope, final LocalDate jobDate, final Long agentFrom,
final Long agentTo, final Long partyFrom, final Long partyTo, final LocalDate generatedOnDate) {
this(jobType, branchScope, jobDate, agentFrom, agentTo, partyFrom, partyTo, generatedOnDate, null);
}
public BatchRun(final String jobType, final String branchScope, final LocalDate jobDate, final Long agentFrom,
final Long agentTo, final Long partyFrom, final Long partyTo, final LocalDate generatedOnDate, final String subType) {
this.jobType = jobType;
this.branchScope = branchScope;
this.subType = subType;
this.jobDate = jobDate;
this.agentFrom = agentFrom;
this.agentTo = agentTo;
this.partyFrom = partyFrom;
this.partyTo = partyTo;
this.status = "CALCULATED";
this.totalRows = 0;
this.totalAmount = BigDecimal.ZERO;
this.generatedOnDate = generatedOnDate;
}
public void addRow(final BatchRunRow row) {
row.setBatchRun(this);
this.rows.add(row);
this.totalRows = this.rows.size();
this.totalAmount = this.totalAmount.add(row.getAmount());
}
public boolean isPosted() {
return STATUS_POSTED.equals(this.status);
}
/** Under way. Set before the work starts, so a second press finds it already running. */
public boolean isPosting() {
return STATUS_POSTING.equals(this.status);
}
public void markPosting() {
this.status = STATUS_POSTING;
this.rowsPosted = 0;
this.failureReason = null;
}
public void recordProgress(final int posted) {
this.rowsPosted = posted;
}
public void markPosted(final LocalDate postedOnDate) {
this.status = STATUS_POSTED;
this.postedOnDate = postedOnDate;
this.rowsPosted = this.totalRows;
this.failureReason = null;
}
/**
* Records why a run stopped, and leaves it able to be run again.
*
* Nothing it had credited stands: the whole run is one transaction, so a failure takes all of it
* back. That is the point - the branch is left with a run that plainly did not happen rather than
* one it has to reconcile by hand.
*/
public void markFailed(final String reason) {
this.status = STATUS_FAILED;
this.failureReason = reason == null ? "Unknown error" : reason.substring(0, Math.min(reason.length(), 500));
}
private static final String STATUS_POSTED = "POSTED";
private static final String STATUS_POSTING = "POSTING";
private static final String STATUS_FAILED = "FAILED";
}
@@ -0,0 +1,130 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.batch.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
/**
* One previewed/posted line of a {@link BatchRun}: for an interest job, one Party's interest; for a
* commission job, one Agent's commission on their collected volume (partyId is null there).
*
* {@code amount} is always what actually gets applied. The remaining figures record how that amount
* was arrived at, which is what the Agent Commission Statement prints and what an agent querying a
* payout needs to see. They are stored rather than recomputed on demand because the inputs move: the
* slab chart can be corrected and the standing deductions change, so a later recomputation would not
* reproduce the figure that was actually paid.
*/
@Getter
@Entity
@Table(name = "m_dds_batch_run_row")
public class BatchRunRow extends AbstractPersistableCustom<Long> {
@ManyToOne
@JoinColumn(name = "batch_run_id", nullable = false)
private BatchRun batchRun;
@Column(name = "party_id")
private Long partyId;
@Column(name = "agent_id", nullable = false)
private Long agentId;
/** What this row applies: interest accrued or posted, or commission net of every deduction. */
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
private BigDecimal amount;
/**
* What the rate was applied to: the collection volume for commission, or the daily product
* (balance summed over the days it was held) for interest.
*/
@Column(name = "base_amount", scale = 6, precision = 19)
private BigDecimal baseAmount;
@Column(name = "rate_percent", scale = 6, precision = 19)
private BigDecimal ratePercent;
/** Before deductions. Equal to {@code amount} on rows that have none. */
@Column(name = "gross_amount", scale = 6, precision = 19)
private BigDecimal grossAmount;
@Column(name = "tds_amount", scale = 6, precision = 19)
private BigDecimal tdsAmount;
@Column(name = "deduction_amount", scale = 6, precision = 19)
private BigDecimal deductionAmount;
@Column(name = "from_date")
private LocalDate fromDate;
@Column(name = "to_date")
private LocalDate toDate;
protected BatchRunRow() {
//
}
private BatchRunRow(final Long partyId, final Long agentId, final BigDecimal amount, final BigDecimal baseAmount,
final BigDecimal ratePercent, final BigDecimal grossAmount, final BigDecimal tdsAmount, final BigDecimal deductionAmount,
final LocalDate fromDate, final LocalDate toDate) {
this.partyId = partyId;
this.agentId = agentId;
this.amount = amount;
this.baseAmount = baseAmount;
this.ratePercent = ratePercent;
this.grossAmount = grossAmount;
this.tdsAmount = tdsAmount;
this.deductionAmount = deductionAmount;
this.fromDate = fromDate;
this.toDate = toDate;
}
/** Interest earned on a Party's balance over the days it was held. Nothing is deducted from it. */
public static BatchRunRow forInterest(final Long partyId, final Long agentId, final BigDecimal amount,
final BigDecimal dailyProduct, final BigDecimal ratePercent, final LocalDate fromDate, final LocalDate toDate) {
return new BatchRunRow(partyId, agentId, amount, dailyProduct, ratePercent, amount, null, null, fromDate, toDate);
}
/** An Agent's commission on their collection volume, net of TDS and the standing deductions. */
public static BatchRunRow forCommission(final Long agentId, final BigDecimal netAmount, final BigDecimal collectionVolume,
final BigDecimal ratePercent, final BigDecimal grossAmount, final BigDecimal tdsAmount, final BigDecimal deductionAmount,
final LocalDate fromDate, final LocalDate toDate) {
return new BatchRunRow(null, agentId, netAmount, collectionVolume, ratePercent, grossAmount, tdsAmount, deductionAmount,
fromDate, toDate);
}
/**
* A posting row: it moves an amount that a prior calculation run already worked out, so there is
* no rate or base to record against it - the run that calculated it holds those.
*/
public static BatchRunRow forPosting(final Long partyId, final Long agentId, final BigDecimal amount, final LocalDate asOnDate) {
return new BatchRunRow(partyId, agentId, amount, null, null, amount, null, null, asOnDate, asOnDate);
}
void setBatchRun(final BatchRun batchRun) {
this.batchRun = batchRun;
}
}
@@ -0,0 +1,32 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.batch.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when Post is attempted on a batch run that has already been posted - a run applies once,
* matching the calculate-then-post safety control (never re-post the same preview).
*/
public class BatchRunAlreadyPostedException extends AbstractPlatformDomainRuleException {
public BatchRunAlreadyPostedException(final Long batchRunId) {
super("error.msg.ddsbatchrun.already.posted", "DDS batch run " + batchRunId + " has already been posted", batchRunId);
}
}
@@ -0,0 +1,28 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.batch.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class BatchRunNotFoundException extends AbstractPlatformResourceNotFoundException {
public BatchRunNotFoundException(final Long id) {
super("error.msg.ddsbatchrun.id.invalid", "DDS batch run with identifier " + id + " does not exist", id);
}
}
@@ -0,0 +1,41 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.batch.exception;
import java.math.BigDecimal;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when an agent's collection volume falls in a gap in the commission chart, so there is no rate
* to pay them at.
*
* The run stops rather than skipping the agent. A gap is a mistake in the chart, and an agent who
* worked all month reading as earning nothing looks exactly like an agent who collected nothing - the
* one failure nobody would query until the agent complained. Naming the agent and the amount that
* found no slab points straight at the range that needs adding.
*/
public class NoCommissionSlabException extends AbstractPlatformDomainRuleException {
public NoCommissionSlabException(final String agentCode, final BigDecimal collectionVolume) {
super("error.msg.ddsbatchrun.no.commission.slab",
"No commission slab covers " + collectionVolume.toPlainString() + ", the collection volume for agent " + agentCode
+ ". Add a slab covering this amount to the Commission Slab Master and run the calculation again.",
agentCode, collectionVolume);
}
}
@@ -0,0 +1,85 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.calculation;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* What a collection agent has earned on a period's collections, and what is left after deductions.
*
* <pre>
* gross the slab's percentage of everything collected
* tds withheld from the gross, when the agent is liable for it
* deductions the four standing amounts the bank recovers from them
* net what the agent is actually paid
* </pre>
*
* Every figure is kept, not just the net: an agent asking why their commission was short needs the
* whole breakdown, and a later change to the slab must not alter what was already paid.
*
* <p>
* Kept apart from the job that runs it so the sum can be checked on its own.
*/
public record DdsCommissionCalculation(BigDecimal collectionVolume, BigDecimal ratePercent, BigDecimal gross, BigDecimal tds,
BigDecimal deductions, BigDecimal net) {
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
private static final int MONEY_SCALE = 2;
/**
* Works out a period's commission.
*
* @param collectionVolume
* everything the agent collected in the period.
* @param ratePercent
* the matching slab's percentage.
* @param tdsApplicable
* whether tax is withheld from this agent at all.
* @param tdsPercent
* the rate withheld when it is.
* @param deductions
* the standing amounts the bank recovers - see {@code Agent.totalDeductions()}.
*/
public static DdsCommissionCalculation of(final BigDecimal collectionVolume, final BigDecimal ratePercent,
final boolean tdsApplicable, final BigDecimal tdsPercent, final BigDecimal deductions) {
final BigDecimal volume = collectionVolume == null ? BigDecimal.ZERO : collectionVolume;
final BigDecimal rate = ratePercent == null ? BigDecimal.ZERO : ratePercent;
final BigDecimal recovered = deductions == null ? BigDecimal.ZERO : deductions;
final BigDecimal gross = volume.multiply(rate).divide(HUNDRED, MONEY_SCALE, RoundingMode.HALF_UP);
final BigDecimal withheld = tdsApplicable && tdsPercent != null
? gross.multiply(tdsPercent).divide(HUNDRED, MONEY_SCALE, RoundingMode.HALF_UP)
: BigDecimal.ZERO;
return new DdsCommissionCalculation(volume, rate, gross, withheld, recovered, gross.subtract(withheld).subtract(recovered));
}
/**
* Whether there is anything to pay.
*
* The deductions can swallow a period's commission entirely. Nothing is owed then, and the agent
* is not put into debt over it - the deductions are standing figures the bank adjusts, not a
* balance a commission run is entitled to carry forward.
*/
public boolean isPayable() {
return this.net.compareTo(BigDecimal.ZERO) > 0;
}
}
@@ -0,0 +1,126 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.calculation;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.List;
/**
* What a DDS account has earned over a period.
*
* Interest is on the <em>daily product</em>: the balance summed over the days it was actually held.
* A customer paying in daily holds each instalment for a different length of time, so interest on
* the closing balance for the whole period would pay them for money they had not yet deposited -
* roughly double what they are owed, which is what this replaced.
*
* This is the same basis {@code Party.calculateMaturityAmount()} quotes at opening, so what the
* interest runs add up to over a term matches the figure the customer was promised.
*
* <p>
* Kept apart from the job that runs it so the sum can be checked on its own. It reads nothing and
* writes nothing - given the same movements it always answers the same, which is what makes it
* worth testing.
*/
public final class DdsInterestCalculation {
/** Wide enough that the intermediate division never loses precision before the final rounding. */
private static final MathContext MATH_CONTEXT = new MathContext(19, RoundingMode.HALF_UP);
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
private static final BigDecimal DAYS_IN_YEAR = BigDecimal.valueOf(365);
private static final int MONEY_SCALE = 2;
private DdsInterestCalculation() {}
/**
* One movement on the account: a collection is positive, a payout negative.
*
* A plain pair rather than the transaction itself, so this sum has no opinion about how
* transactions are stored and can be checked with a handful of literals.
*/
public record Movement(LocalDate date, BigDecimal signedAmount) {}
/**
* The balance summed over the days it was held, between {@code from} (exclusive) and {@code to}
* (inclusive).
*
* @param openingBalance
* what the account held before any of these movements.
* @param movements
* every movement up to the end of the period, oldest first. Anything dated on or
* before {@code from} has already happened and makes up the balance this period opens
* with rather than a movement within it.
*/
public static BigDecimal dailyProduct(final BigDecimal openingBalance, final List<Movement> movements, final LocalDate from,
final LocalDate to) {
BigDecimal balance = openingBalance == null ? BigDecimal.ZERO : openingBalance;
int index = 0;
while (index < movements.size() && !movements.get(index).date().isAfter(from)) {
balance = balance.add(movements.get(index).signedAmount());
index++;
}
BigDecimal product = BigDecimal.ZERO;
LocalDate cursor = from;
while (index < movements.size()) {
final LocalDate movementDate = movements.get(index).date();
if (movementDate.isAfter(to)) {
break;
}
// The untouched days between the last movement and this one, at the balance they stood at.
final long daysAtOpeningBalance = ChronoUnit.DAYS.between(cursor, movementDate) - 1;
if (daysAtOpeningBalance > 0) {
product = product.add(balance.multiply(BigDecimal.valueOf(daysAtOpeningBalance)));
}
// Apply every movement dated that day before counting the day itself, so a deposit earns
// from the day it was made.
while (index < movements.size() && movementDate.equals(movements.get(index).date())) {
balance = balance.add(movements.get(index).signedAmount());
index++;
}
product = product.add(balance);
cursor = movementDate;
}
final long remainingDays = ChronoUnit.DAYS.between(cursor, to);
if (remainingDays > 0) {
product = product.add(balance.multiply(BigDecimal.valueOf(remainingDays)));
}
return product;
}
/** Simple interest on a daily product at an annual rate, rounded to paise. */
public static BigDecimal interestOn(final BigDecimal dailyProduct, final BigDecimal ratePercent) {
if (dailyProduct == null || ratePercent == null || dailyProduct.compareTo(BigDecimal.ZERO) <= 0
|| ratePercent.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO.setScale(MONEY_SCALE);
}
return dailyProduct.multiply(ratePercent).divide(HUNDRED.multiply(DAYS_IN_YEAR), MATH_CONTEXT).setScale(MONEY_SCALE,
RoundingMode.HALF_UP);
}
}
@@ -0,0 +1,117 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.calculation;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
/**
* Where a daily deposit account stands against the instalments it owes.
*
* The bank's rule has two sides and they are not symmetrical. A customer may run ahead, but only a few
* days - the scheme is a habit of saving daily, not a deposit paid in one go. A customer may fall
* behind and catch up, but only within a short window; instalments older than that are skipped, and
* skipped means gone - the agent does not go back for them, and the account matures on what was
* actually paid.
*
* So a shortfall is split rather than shown as one figure. What can still be collected is worth a
* clerk chasing today; what has passed the window is not, and showing them together would have the
* clerk asking for money the bank has already written off.
*
* Kept here, outside either service, because the screen that shows these figures and the check that
* refuses a collection exceeding them have to agree. Two copies would drift, and the day they did the
* screen would quote a customer an amount the server then refused.
*
* @param expected what should have been collected by the day this was worked out
* @param advance signed: positive is paid ahead, negative is behind
* @param advanceDays whole instalments covered ahead - a part-paid day does not count
* @param arrearsDays whole or part instalments behind
* @param recoverable the part of a shortfall still inside the catch-up window
* @param skippedDays instalments past that window
* @param skipped what those come to
*/
public record DdsSchedule(BigDecimal expected, BigDecimal advance, int advanceDays, int arrearsDays, BigDecimal recoverable,
int skippedDays, BigDecimal skipped) {
/**
* How far behind an account may fall and still be caught up. Past this the instalments are
* skipped - the agent does not go back for them and the customer matures on less.
*/
public static final int CATCH_UP_DAYS = 3;
/**
* The most a customer may pay ahead, so a scheme is not simply prepaid into a lump deposit. A few
* days is the bank's allowance for someone going away or paid weekly.
*/
public static final int MAX_ADVANCE_DAYS = 5;
private static final DdsSchedule NOTHING_DUE = new DdsSchedule(BigDecimal.ZERO, BigDecimal.ZERO, 0, 0, BigDecimal.ZERO, 0,
BigDecimal.ZERO);
/**
* How many instalments should have been paid by the given day.
*
* The first falls on the opening day itself - the same day the maturity figure starts counting
* from, so the two agree - and the count stops at the term's own length, so a matured account does
* not keep falling further behind.
*/
public static int instalmentsDue(final LocalDate openingDate, final Integer termDays, final LocalDate on) {
if (openingDate == null || termDays == null || termDays <= 0 || on == null) {
return 0;
}
final long elapsed = ChronoUnit.DAYS.between(openingDate, on) + 1;
return (int) Math.max(0, Math.min(termDays, elapsed));
}
public static DdsSchedule of(final BigDecimal daily, final int instalmentsDue, final BigDecimal collected) {
if (daily == null || daily.compareTo(BigDecimal.ZERO) <= 0) {
// Nothing is due daily, so there is no schedule to be ahead or behind of.
return NOTHING_DUE;
}
final BigDecimal paid = collected == null ? BigDecimal.ZERO : collected;
final BigDecimal expected = daily.multiply(BigDecimal.valueOf(instalmentsDue));
final BigDecimal advance = paid.subtract(expected);
if (advance.compareTo(BigDecimal.ZERO) >= 0) {
// Ahead. Only whole instalments count as days covered - a customer eighty rupees into a
// hundred-rupee day still owes today, and the agent should still call.
final int daysCovered = advance.divideToIntegralValue(daily).intValue();
return new DdsSchedule(expected, advance, daysCovered, 0, BigDecimal.ZERO, 0, BigDecimal.ZERO);
}
// Behind. A part-paid day counts as a day behind for the same reason.
final BigDecimal shortfall = advance.negate();
final int daysBehind = shortfall.divide(daily, 0, RoundingMode.CEILING).intValue();
final BigDecimal window = daily.multiply(BigDecimal.valueOf(CATCH_UP_DAYS));
final BigDecimal recoverable = shortfall.min(window);
return new DdsSchedule(expected, advance, 0, daysBehind, recoverable, Math.max(0, daysBehind - CATCH_UP_DAYS),
shortfall.subtract(recoverable));
}
/** Whole instalments a customer may still pay off, which is what the arrears option may offer. */
public int recoverableDays(final BigDecimal daily) {
if (daily == null || daily.compareTo(BigDecimal.ZERO) <= 0) {
return 0;
}
return this.recoverable.divideToIntegralValue(daily).intValue();
}
}
@@ -0,0 +1,53 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.collection.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import lombok.Builder;
import lombok.Data;
/**
* An agent's day, as the clerk sees it when the agent walks up to the counter.
*
* The figure that matters is {@code expectedAmount}: what the agent's own entries add up to, and so
* what should be in their bag. It is given as a total beside the lines it came from, because a clerk
* who is short counts the lines to find out where.
*/
@Data
@Builder
public class AgentCollectionSummaryData {
private final Long agentId;
private final String agentCode;
private final String agentName;
private final LocalDate entryDate;
private final int pendingCount;
/** What the agent's outstanding entries add up to - the cash the branch should be handed. */
private final BigDecimal expectedAmount;
/** What has already been counted and posted for this agent on this date. */
private final int settledCount;
private final BigDecimal settledAmount;
private final List<CollectionEntryData> entries;
}
@@ -0,0 +1,66 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.collection.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Builder;
import lombok.Data;
/**
* One written-down collection, as the agent's screen and the clerk's settlement list show it.
*
* Carries the party's code, name and balance beside the amount, because both readers are checking
* the line against a person rather than an id - the agent against the customer they just left, the
* clerk against the passbook in front of them.
*/
@Data
@Builder
public class CollectionEntryData {
private final Long id;
private final Long agentId;
private final String agentCode;
private final String agentName;
private final Long partyId;
private final String partyCode;
private final String partyName;
/** The account's balance as it stands now - before this entry, which has not been posted. */
private final BigDecimal partyBalance;
/** What this scheme takes each day, so an amount that is not a day's instalment stands out. */
private final BigDecimal dailyDepositAmount;
private final LocalDate entryDate;
private final BigDecimal amount;
private final String transactionMode;
private final String chequeNo;
private final String tokenNo;
private final String description;
private final String status;
private final LocalDate settledOn;
private final Long partyTransactionId;
/** Days paid ahead, or days of arrears cleared - empty on an ordinary day's collection. */
private final Integer advanceDays;
private final Integer arrearsDays;
}
@@ -0,0 +1,174 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.collection.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
import org.apache.fineract.organisation.agent.domain.Agent;
import org.apache.fineract.portfolio.dds.party.domain.Party;
/**
* One line an agent wrote down on their round: this customer, this much, in this form.
*
* It is not a transaction and deliberately reads nothing like one. No balance is carried on it, no
* ledger entry stands behind it, and no report of the bank's position counts it. It says only what
* the agent says they collected - the bank's own record of it begins when a clerk has counted the
* cash and confirmed the day's entries, and what that writes is an ordinary
* {@code m_dds_party_transaction} row.
*
* The distinction is the whole point of the table. Credited on the spot, the books showed customers
* paid in notes the branch had never held, and there was nothing to count the agent's bag against
* when they arrived. Held here, the agent's word and the bank's books stay separate until somebody
* puts them side by side.
*/
@Getter
@Entity
@Table(name = "m_dds_collection_entry")
public class CollectionEntry extends AbstractAuditableCustom {
/** Waiting for a clerk to count the cash behind it. */
public static final String PENDING = "PENDING";
/** Counted, posted, and now standing behind a real transaction. */
public static final String SETTLED = "SETTLED";
/** Withdrawn before it was ever posted - a wrong line the agent took back. */
public static final String CANCELLED = "CANCELLED";
@ManyToOne
@JoinColumn(name = "agent_id", nullable = false)
private Agent agent;
@ManyToOne
@JoinColumn(name = "party_id", nullable = false)
private Party party;
/**
* The day the money changed hands, which is the agent's day and not the branch's. A round
* collected on Friday and brought in on Monday is still Friday's collection, and the customer's
* statement has to say so.
*/
@Column(name = "entry_date", nullable = false)
private LocalDate entryDate;
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
private BigDecimal amount;
@Column(name = "transaction_mode", length = 20, nullable = false)
private String transactionMode;
@Column(name = "cheque_no", length = 50)
private String chequeNo;
@Column(name = "token_no", length = 50)
private String tokenNo;
@Column(name = "description", length = 200)
private String description;
@Column(name = "status", length = 20, nullable = false)
private String status;
@Column(name = "settled_on")
private LocalDate settledOn;
/**
* The transaction this entry became, once it was posted.
*
* Held as a plain id rather than a mapped association: this table is the paper the bank checked
* against, not part of the ledger, and a link that pulled a transaction in whenever an entry was
* read would make the two look like halves of one thing. Anyone who needs the transaction can
* fetch it; most callers only need to know there is one.
*/
@Column(name = "party_transaction_id")
private Long partyTransactionId;
/**
* The application of the cashier's DDS receipts that settled this entry, when it was settled that
* way - see 0517, which records which vouchers it used. A plain id for the same reason as the
* transaction.
*/
@Column(name = "receipt_application_id")
private Long receiptApplicationId;
/** Days paid ahead, or days of arrears cleared - see 0520. Both empty on an ordinary day's collection. */
@Column(name = "advance_days")
private Integer advanceDays;
@Column(name = "arrears_days")
private Integer arrearsDays;
protected CollectionEntry() {
//
}
public CollectionEntry(final Agent agent, final Party party, final LocalDate entryDate, final BigDecimal amount,
final String transactionMode, final String chequeNo, final String tokenNo, final String description) {
this.agent = agent;
this.party = party;
this.entryDate = entryDate;
this.amount = amount;
this.transactionMode = transactionMode;
this.chequeNo = chequeNo;
this.tokenNo = tokenNo;
this.description = description;
this.status = PENDING;
}
/** Records what the collection covers beyond today, once its amount has been checked against it. */
public void covering(final Integer advanceDays, final Integer arrearsDays) {
this.advanceDays = advanceDays;
this.arrearsDays = arrearsDays;
}
public boolean isPending() {
return PENDING.equals(this.status);
}
/**
* Records that this entry has been counted and posted, and what it became.
*
* The transaction's id is kept so the two can always be put back together - a clerk asked six
* months later which entry a credit came from has an answer, rather than a matching exercise on
* date and amount.
*/
public void markSettled(final LocalDate settledOn, final Long partyTransactionId) {
this.status = SETTLED;
this.settledOn = settledOn;
this.partyTransactionId = partyTransactionId;
}
/** Settled against the cashier's receipts: the same as {@link #markSettled}, and which application did it. */
public void markSettledByReceipt(final LocalDate settledOn, final Long partyTransactionId, final Long receiptApplicationId) {
markSettled(settledOn, partyTransactionId);
this.receiptApplicationId = receiptApplicationId;
}
/** Withdraws a line that was never posted. Kept, not deleted - the agent wrote it, so it stands. */
public void markCancelled() {
this.status = CANCELLED;
}
}
@@ -0,0 +1,28 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.collection.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class CollectionEntryNotFoundException extends AbstractPlatformResourceNotFoundException {
public CollectionEntryNotFoundException(final Long entryId) {
super("error.msg.dds.collection.entry.not.found", "Collection entry " + entryId + " does not exist", entryId);
}
}
@@ -0,0 +1,37 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.collection.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when a written-down collection is taken back after it has already been settled.
*
* By then it is not a note any more - a customer has been credited and an agent's cash recorded
* against it. Withdrawing it here would leave the note cancelled and the credit standing, which is
* the one state nobody could reconcile. A posted collection is undone the way any posting is: with
* an entry the other way, on the ledger, where it can be seen.
*/
public class CollectionEntryNotPendingException extends AbstractPlatformDomainRuleException {
public CollectionEntryNotPendingException(final Long entryId, final String status) {
super("error.msg.dds.collection.entry.not.pending",
"Collection entry " + entryId + " is " + status.toLowerCase() + " and can no longer be cancelled", entryId, status);
}
}
@@ -0,0 +1,36 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.collection.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when a settlement is asked for and the agent has nothing outstanding on that date.
*
* Said plainly rather than passed off as a settlement of nothing, because the clerk is standing at
* the counter with an agent in front of them: either they have the wrong date, or the round has been
* settled already, and both are worth knowing before the cash is taken.
*/
public class NothingToSettleException extends AbstractPlatformDomainRuleException {
public NothingToSettleException(final String agentCode, final String entryDate) {
super("error.msg.dds.collection.nothing.to.settle",
"Agent " + agentCode + " has no collection entries outstanding for " + entryDate, agentCode, entryDate);
}
}
@@ -0,0 +1,182 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.party.data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import lombok.Builder;
import lombok.Getter;
/**
* Immutable data object representing a DDS Party (customer scheme account), including its nominees.
* Built via the Lombok builder rather than a long positional constructor - the field count here makes
* positional args unreadable and error-prone to keep in sync.
*/
@Getter
@Builder(toBuilder = true)
public final class PartyData implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private final Long id;
private final Long clientId;
private final String clientName;
private final Long agentId;
private final String agentCode;
private final String agentName;
/** The branch this account belongs to - its own, not the collecting agent's. */
private final Long officeId;
private final String officeName;
/**
* The customer's real deposit account, opened when the party was created.
*
* The id alone was carried here before, which is a number only the database means anything by -
* a screen showing "36" tells an officer nothing. The account's own number, product, status and
* balance are read from the account itself, so what a party shows is what the bank's books say
* rather than a second copy that can drift. All four are null for a party opened before DDS
* began opening real accounts, and that is worth showing plainly rather than hiding.
*/
private final Long savingsAccountId;
private final String savingsAccountNo;
private final String savingsProductName;
private final String savingsAccountStatus;
private final BigDecimal savingsAccountBalance;
/**
* What on this account is pledged against a loan and cannot leave.
*
* Shown beside the balance on the closure list because a deposit standing as security is owed but
* not payable, and a clerk who reads only the balance would try to pay it and be refused.
*/
private final BigDecimal amountHeldUnderLien;
private final String subType;
/**
* The account type's description, resolved from the DDS Account Type master that {@code subType}
* holds the code of - so callers can show the scheme by the name officers know it by without
* fetching the master separately.
*/
private final String subTypeDescription;
private final String partyCode;
/** The DDS account's own number. */
private final String accountNo;
private final String lastAccount;
private final Boolean nominal;
private final String aadharNo;
private final String partyName;
private final String partyName1;
private final String address1;
private final String address2;
private final String address3;
private final String city;
private final String pincode;
private final String gender;
private final String panCardNo;
private final String telephone;
private final String mobileNoSms;
private final String operation;
private final LocalDate birthDate;
private final String occupation;
private final String risk;
private final String ckycId;
private final String adultMinor;
private final LocalDate minorDate;
/**
* The day a minor account holder comes of age, and whether it has passed.
*
* Worked out from the birth date rather than taken from {@code minorDate}, which is typed in by
* hand and has never been filled on any record. Null on an adult's account.
*
* The guardian's authority to operate the account ends on that day, so the branch has to take
* fresh KYC, a signature and a new mandate from the customer themselves. Nothing acts on this
* yet - whether the account should also be restricted until that is done is the bank's rule, and
* blocking one wrongly would strand a customer's money. It is carried here so the day stops
* passing unnoticed, which is what happened before.
*/
private final LocalDate majorOn;
private final boolean minorTurnedMajor;
private final Long guardianClientId;
private final String minAccountOpt;
private final String relation;
private final LocalDate openingDate;
private final Integer month;
private final LocalDate maturityDate;
private final LocalDate reopenDate;
private final BigDecimal depositAmount;
private final BigDecimal dailyDepositAmount;
private final Integer days;
private final String ledgerNo;
private final BigDecimal maturityAmount;
private final BigDecimal interestRate;
private final String withdrawType;
private final Boolean od1Flag;
private final Boolean od2Flag;
private final Boolean lien;
/** Why the money is held - null when it is not. */
private final String lienReason;
private final LocalDate payDate;
private final BigDecimal paymentAmount;
private final BigDecimal currentBalance;
private final BigDecimal openingBalance;
/**
* Where this account stands against the instalments it should have paid by today.
*
* A daily scheme is a promise to pay a fixed sum every day, and the only figure the screen used to
* show was the running balance - which says how much has been paid but not whether it was paid on
* time. A customer eleven instalments behind and one who is four ahead can hold the same balance.
*
* {@code advanceAmount} is signed: positive is paid ahead, negative is behind. The rest of these
* read it in the bank's own terms - how many days that comes to, how much of a shortfall can still
* be recovered within the catch-up window, and how much has fallen outside it and is gone.
*/
private final Integer instalmentsDue;
private final BigDecimal expectedToDate;
private final BigDecimal collectedToDate;
private final BigDecimal advanceAmount;
private final Integer advanceDays;
private final Integer arrearsDays;
private final BigDecimal recoverableAmount;
/** Instalments now past the catch-up window. These are not collected again - they are lost. */
private final Integer skippedDays;
private final BigDecimal skippedAmount;
private final Boolean isSmsActive;
private final Boolean isEmailActive;
/** Interest worked out but not yet credited, and how far it is calculated through. */
private final BigDecimal interestCalc;
private final LocalDate interestDate;
/** The same pair for the OD interest stream, which runs and posts independently. */
private final BigDecimal odInterestCalc;
private final LocalDate odInterestDate;
private final String spDescription;
private final String freezeCode;
private final String advCode;
private final String creditToBranch;
private final String creditToAccountType;
private final String creditToAccount;
private final Boolean nomineeApplicable;
private final String nomineeSplitMode;
private final String status;
private final LocalDate closeDate;
private final Collection<PartyNomineeData> nominees;
}
@@ -0,0 +1,69 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.party.data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
@Getter
public final class PartyNomineeData implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private final Long id;
private final LocalDate nominationDate;
private final String name;
private final String mobileNo;
private final LocalDate birthDate;
private final Boolean minor;
private final String relation;
private final String gender;
private final String guardianName;
/** Null under a Sequence split, where the order the nominees are paid in is the entitlement. */
private final BigDecimal sharePercent;
/** Null under a Percentage split, where everyone is paid at once and there is no first. */
private final Integer sequenceNo;
public static PartyNomineeData instance(final Long id, final LocalDate nominationDate, final String name, final String mobileNo,
final LocalDate birthDate, final Boolean minor, final String relation, final String gender, final String guardianName,
final BigDecimal sharePercent, final Integer sequenceNo) {
return new PartyNomineeData(id, nominationDate, name, mobileNo, birthDate, minor, relation, gender, guardianName, sharePercent,
sequenceNo);
}
private PartyNomineeData(final Long id, final LocalDate nominationDate, final String name, final String mobileNo,
final LocalDate birthDate, final Boolean minor, final String relation, final String gender, final String guardianName,
final BigDecimal sharePercent, final Integer sequenceNo) {
this.id = id;
this.nominationDate = nominationDate;
this.name = name;
this.mobileNo = mobileNo;
this.birthDate = birthDate;
this.minor = minor;
this.relation = relation;
this.gender = gender;
this.guardianName = guardianName;
this.sharePercent = sharePercent;
this.sequenceNo = sequenceNo;
}
}
@@ -0,0 +1,791 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.party.domain;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderBy;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
import org.apache.fineract.organisation.agent.domain.Agent;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.client.domain.Client;
/**
* A DDS Party - the customer's daily-deposit scheme account, linked to the Agent who collects for it.
* Carries the scheme's KYC, terms and two-stage overdue flags, plus a nominee list.
*
* The underlying RD (recurring deposit) account this scheme conceptually reuses is not wired up yet -
* {@code savingsAccountId} is a nullable placeholder for that future linkage (see the DDS architecture
* decision to reuse the RD engine for the deposit half). Until then the scheme/balance fields below
* are this record's own, not mirrored from any m_savings_account row.
*/
@Getter
@Entity
@Table(name = "m_dds_party", uniqueConstraints = { @UniqueConstraint(columnNames = { "party_code" }, name = "party_code_UNIQUE") })
public class Party extends AbstractAuditableCustom {
/** DDS accounts carry a flat interest rate unless the officer explicitly overrides it at creation. */
private static final BigDecimal DEFAULT_INTEREST_RATE = new BigDecimal("2");
/** Interest is quoted per annum, so the daily instalments are pro-rated over a year. */
private static final int DAYS_IN_YEAR = 365;
private static final MathContext MATH_CONTEXT = new MathContext(19, RoundingMode.HALF_UP);
public static final String STATUS_ACTIVE = "ACTIVE";
public static final String STATUS_CLOSED = "CLOSED";
@ManyToOne
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@ManyToOne
@JoinColumn(name = "agent_id", nullable = false)
private Agent agent;
/**
* The branch this account belongs to.
*
* Its own, not the collecting agent's. An agent working out of one branch can hold accounts for
* customers of another, and reassigning an account to a different agent should not move the
* customer's branch with it. Defaults to the agent's branch when the caller does not name one,
* which is what the account was read as before it had a branch of its own.
*/
@ManyToOne
@JoinColumn(name = "office_id")
private Office office;
@Column(name = "savings_account_id")
private Long savingsAccountId;
@Column(name = "sub_type", length = 60, nullable = false)
private String subType;
@Column(name = "party_code", length = 20, nullable = false)
private String partyCode;
/** The DDS account's own number - see 0514. Given once the party has an id, and never changed. */
@Column(name = "account_no", length = 20)
private String accountNo;
@Column(name = "last_account", length = 50)
private String lastAccount;
@Column(name = "nominal", nullable = false)
private boolean nominal;
@Column(name = "aadhar_no", length = 20)
private String aadharNo;
@Column(name = "party_name", length = 100, nullable = false)
private String partyName;
@Column(name = "party_name1", length = 100)
private String partyName1;
@Column(name = "address1", length = 200, nullable = false)
private String address1;
@Column(name = "address2", length = 200)
private String address2;
@Column(name = "address3", length = 200)
private String address3;
@Column(name = "city", length = 100, nullable = false)
private String city;
@Column(name = "pincode", length = 20, nullable = false)
private String pincode;
@Column(name = "gender", length = 20)
private String gender;
@Column(name = "pan_card_no", length = 20)
private String panCardNo;
@Column(name = "telephone", length = 20)
private String telephone;
@Column(name = "mobile_no_sms", length = 20, nullable = false)
private String mobileNoSms;
@Column(name = "operation", length = 20, nullable = false)
private String operation;
@Column(name = "birth_date")
private LocalDate birthDate;
@Column(name = "occupation", length = 40)
private String occupation;
@Column(name = "risk", length = 20)
private String risk;
@Column(name = "ckyc_id", length = 40)
private String ckycId;
@Column(name = "adult_minor", length = 20, nullable = false)
private String adultMinor;
@Column(name = "minor_date")
private LocalDate minorDate;
@Column(name = "guardian_client_id")
private Long guardianClientId;
@Column(name = "min_account_opt", length = 40)
private String minAccountOpt;
@Column(name = "relation", length = 40)
private String relation;
@Column(name = "opening_date", nullable = false)
private LocalDate openingDate;
@Column(name = "months")
private Integer month;
@Column(name = "maturity_date")
private LocalDate maturityDate;
@Column(name = "reopen_date")
private LocalDate reopenDate;
@Column(name = "deposit_amount", scale = 6, precision = 19, nullable = false)
private BigDecimal depositAmount;
@Column(name = "daily_deposit_amount", scale = 6, precision = 19, nullable = false)
private BigDecimal dailyDepositAmount;
@Column(name = "days", nullable = false)
private Integer days;
@Column(name = "ledger_no", length = 40)
private String ledgerNo;
@Column(name = "maturity_amount", scale = 6, precision = 19, nullable = false)
private BigDecimal maturityAmount;
@Column(name = "interest_rate", scale = 6, precision = 19, nullable = false)
private BigDecimal interestRate;
@Column(name = "withdraw_type", length = 20)
private String withdrawType;
@Column(name = "od1_flag", nullable = false)
private boolean od1Flag;
@Column(name = "od2_flag", nullable = false)
private boolean od2Flag;
/**
* Whether this account's money is being held, and so cannot leave.
*
* Deposits are unaffected - the agent collects as before and interest posts as before. What is
* refused is every way out: the maturity payout, an early closure, and a transfer to another
* party. A lien that stopped the collections would defeat the scheme it is securing.
*/
@Column(name = "lien", nullable = false)
private boolean lien;
/**
* Why the money is held.
*
* The flag alone says an account is frozen and never says what for, which is how a held deposit
* outlives the loan it secured: months later nobody can tell whether it has been repaid, so
* nobody will release it and nobody can justify keeping it.
*
* It falls to a line of text because DDS and the loan module are not connected - nothing else in
* the system links a held deposit to the loan behind it.
*/
@Column(name = "lien_reason", length = 200)
private String lienReason;
@Column(name = "pay_date")
private LocalDate payDate;
@Column(name = "payment_amount", scale = 6, precision = 19, nullable = false)
private BigDecimal paymentAmount;
@Column(name = "current_balance", scale = 6, precision = 19, nullable = false)
private BigDecimal currentBalance;
@Column(name = "opening_balance", scale = 6, precision = 19, nullable = false)
private BigDecimal openingBalance;
@Column(name = "is_sms_active", nullable = false)
private boolean isSmsActive;
@Column(name = "is_email_active", nullable = false)
private boolean isEmailActive;
/** Interest worked out but not yet credited - the legacy screen's "Int.Calc". */
@Column(name = "interest_calc", scale = 6, precision = 19, nullable = false)
private BigDecimal interestCalc;
/** How far regular interest has been calculated through. */
@Column(name = "interest_date")
private LocalDate interestDate;
/**
* OD interest is a second stream over the same account, run and posted independently of regular
* interest, so it keeps its own accrued figure and through-date rather than sharing them.
*/
@Column(name = "od_interest_calc", scale = 6, precision = 19, nullable = false)
private BigDecimal odInterestCalc;
@Column(name = "od_interest_date")
private LocalDate odInterestDate;
@Column(name = "sp_description", length = 500)
private String spDescription;
@Column(name = "freeze_code", length = 20, nullable = false)
private String freezeCode;
@Column(name = "adv_code", length = 40, nullable = false)
private String advCode;
@Column(name = "credit_to_branch", length = 100)
private String creditToBranch;
@Column(name = "credit_to_account_type", length = 100)
private String creditToAccountType;
@Column(name = "credit_to_account", length = 50)
private String creditToAccount;
@Column(name = "nominee_applicable", nullable = false)
private boolean nomineeApplicable;
@Column(name = "nominee_split_mode", length = 20)
private String nomineeSplitMode;
@Column(name = "status", length = 20, nullable = false)
private String status;
@Column(name = "close_date")
private LocalDate closeDate;
@OneToMany(mappedBy = "party", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@OrderBy("id")
private List<PartyNominee> nominees = new ArrayList<>();
protected Party() {
//
}
private Party(final Client client, final Agent agent, final Office office, final String partyCode, final JsonCommand command) {
this.client = client;
this.agent = agent;
this.office = office;
this.partyCode = partyCode;
this.subType = command.stringValueOfParameterNamed("subType");
this.lastAccount = command.stringValueOfParameterNamedAllowingNull("lastAccount");
this.nominal = command.booleanPrimitiveValueOfParameterNamed("nominal");
this.aadharNo = command.stringValueOfParameterNamedAllowingNull("aadharNo");
this.partyName = command.stringValueOfParameterNamed("partyName");
this.partyName1 = command.stringValueOfParameterNamedAllowingNull("partyName1");
this.address1 = command.stringValueOfParameterNamed("address1");
this.address2 = command.stringValueOfParameterNamedAllowingNull("address2");
this.address3 = command.stringValueOfParameterNamedAllowingNull("address3");
this.city = command.stringValueOfParameterNamed("city");
this.pincode = command.stringValueOfParameterNamed("pincode");
this.gender = command.stringValueOfParameterNamedAllowingNull("gender");
this.panCardNo = command.stringValueOfParameterNamedAllowingNull("panCardNo");
this.telephone = command.stringValueOfParameterNamedAllowingNull("telephone");
this.mobileNoSms = command.stringValueOfParameterNamed("mobileNoSms");
this.operation = command.stringValueOfParameterNamed("operation");
this.birthDate = command.localDateValueOfParameterNamed("birthDate");
this.occupation = command.stringValueOfParameterNamedAllowingNull("occupation");
this.risk = command.stringValueOfParameterNamedAllowingNull("risk");
this.ckycId = command.stringValueOfParameterNamedAllowingNull("ckycId");
this.adultMinor = command.stringValueOfParameterNamed("adultMinor");
this.minorDate = command.localDateValueOfParameterNamed("minorDate");
this.guardianClientId = command.longValueOfParameterNamed("guardianClientId");
this.minAccountOpt = command.stringValueOfParameterNamedAllowingNull("minAccountOpt");
this.relation = command.stringValueOfParameterNamedAllowingNull("relation");
this.openingDate = command.localDateValueOfParameterNamed("openingDate");
this.month = command.integerValueOfParameterNamedDefaultToNullIfZero("month");
// maturityDate and maturityAmount are not read from the request - recalculateTerm() below
// works both out from the opening date and the terms.
this.reopenDate = command.localDateValueOfParameterNamed("reopenDate");
this.depositAmount = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("depositAmount"));
this.dailyDepositAmount = command.bigDecimalValueOfParameterNamed("dailyDepositAmount");
this.days = command.integerValueOfParameterNamed("days");
this.ledgerNo = command.stringValueOfParameterNamedAllowingNull("ledgerNo");
this.maturityAmount = BigDecimal.ZERO;
this.interestRate = command.parameterExists("interestRate")
? command.bigDecimalValueOfParameterNamed("interestRate")
: DEFAULT_INTEREST_RATE;
this.withdrawType = command.stringValueOfParameterNamedAllowingNull("withdrawType");
this.od1Flag = false;
this.od2Flag = false;
this.lien = command.booleanPrimitiveValueOfParameterNamed("lien");
this.lienReason = StringUtils.trimToNull(command.stringValueOfParameterNamedAllowingNull("lienReason"));
this.payDate = command.localDateValueOfParameterNamed("payDate");
this.paymentAmount = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("paymentAmount"));
this.openingBalance = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("openingBalance"));
this.currentBalance = this.openingBalance;
this.isSmsActive = command.booleanPrimitiveValueOfParameterNamed("isSmsActive");
this.isEmailActive = command.booleanPrimitiveValueOfParameterNamed("isEmailActive");
this.interestCalc = BigDecimal.ZERO;
this.interestDate = command.localDateValueOfParameterNamed("interestDate");
this.odInterestCalc = BigDecimal.ZERO;
this.odInterestDate = null;
this.spDescription = command.stringValueOfParameterNamedAllowingNull("spDescription");
this.freezeCode = StringUtils.defaultIfBlank(command.stringValueOfParameterNamedAllowingNull("freezeCode"), "Normal");
this.advCode = StringUtils.defaultIfBlank(command.stringValueOfParameterNamedAllowingNull("advCode"), "None");
this.creditToBranch = command.stringValueOfParameterNamedAllowingNull("creditToBranch");
this.creditToAccountType = command.stringValueOfParameterNamedAllowingNull("creditToAccountType");
this.creditToAccount = command.stringValueOfParameterNamedAllowingNull("creditToAccount");
this.nomineeApplicable = command.booleanPrimitiveValueOfParameterNamed("nomineeApplicable");
this.nomineeSplitMode = StringUtils.defaultIfBlank(command.stringValueOfParameterNamedAllowingNull("nomineeSplitMode"),
"Percentage");
this.status = STATUS_ACTIVE;
recalculateTerm();
}
/**
* Takes the scheme's interest rate. The rate belongs to the scheme, not to the counter: it is set
* when the account is opened or moved to another scheme, and again on every open account whenever
* the bank changes the scheme's rate, with the maturity figure worked out again on it.
*/
public void takeSchemeRate(final BigDecimal schemeRate) {
this.interestRate = schemeRate == null ? BigDecimal.ZERO : schemeRate;
recalculateTerm();
}
/**
* Works out the maturity date and amount from the terms just set.
*
* Both are quoted to the customer when the account is opened, so neither is taken from the
* request: a figure the caller supplies is a figure the caller can get wrong or tamper with.
* The screen shows the same numbers as a preview, worked out the same way.
*/
private void recalculateTerm() {
if (this.openingDate != null) {
if (this.month != null && this.month > 0) {
this.maturityDate = this.openingDate.plusMonths(this.month);
// Collection runs every day of the term without a break, so the number of instalments
// is the number of days - it is not a separate thing for anyone to type.
//
// It was asked for, and a figure below the term's own length quietly broke the
// maturity quote: an account collecting a hundred rupees a day for thirty days with
// ten instalments keyed against it was quoted a thousand rupees and would collect
// three. Working it out here means the two can no longer disagree.
this.days = (int) ChronoUnit.DAYS.between(this.openingDate, this.maturityDate);
} else if (this.days != null && this.days > 0) {
// No month given, so the term is the run of days itself and days stands as entered.
this.maturityDate = this.openingDate.plusDays(this.days);
}
}
this.maturityAmount = calculateMaturityAmount();
}
/**
* What the account matures to: every instalment paid in, plus simple interest on each for the
* time it sits until maturity.
*
* The first instalment earns over the whole term and the last over none, which comes to
* n(n+1)/2 instalment-days of interest across n instalments. At a zero rate this is just the
* instalments themselves.
*/
private BigDecimal calculateMaturityAmount() {
if (this.dailyDepositAmount == null || this.days == null || this.days <= 0
|| this.dailyDepositAmount.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO;
}
final BigDecimal instalments = BigDecimal.valueOf(this.days);
final BigDecimal principal = this.dailyDepositAmount.multiply(instalments);
final BigDecimal rate = this.interestRate == null ? BigDecimal.ZERO : this.interestRate;
if (rate.compareTo(BigDecimal.ZERO) <= 0) {
return principal.setScale(2, RoundingMode.HALF_UP);
}
final BigDecimal instalmentDays = instalments.multiply(instalments.add(BigDecimal.ONE))
.divide(BigDecimal.valueOf(2), MATH_CONTEXT);
final BigDecimal interest = this.dailyDepositAmount.multiply(instalmentDays).multiply(rate)
.divide(BigDecimal.valueOf(100), MATH_CONTEXT)
.divide(BigDecimal.valueOf(DAYS_IN_YEAR), MATH_CONTEXT);
return principal.add(interest).setScale(2, RoundingMode.HALF_UP);
}
public static Party fromJson(final Client client, final Agent agent, final Office office, final String partyCode,
final JsonCommand command) {
return new Party(client, agent, office, partyCode, command);
}
/** Moves this account to another branch, which a party update may do on its own. */
public void changeOffice(final Office newOffice) {
this.office = newOffice;
}
private static BigDecimal orZero(final BigDecimal value) {
return value == null ? BigDecimal.ZERO : value;
}
public void addNominee(final PartyNominee nominee) {
nominee.setParty(this);
this.nominees.add(nominee);
}
public void resetNominees() {
this.nominees.clear();
}
public void changeClient(final Client newClient) {
this.client = newClient;
}
public void changeAgent(final Agent newAgent) {
this.agent = newAgent;
}
/**
* Links this Party to an existing real Savings Account, resolved by account number in the write
* service. There is no unlink - once a Party's DDS scheme is tied to a real account, it stays
* tied.
*/
public void linkSavingsAccount(final Long savingsAccountId) {
this.savingsAccountId = savingsAccountId;
}
/** Numbers the account from its id, the same way 0514 numbered the accounts already open. */
public void assignAccountNo() {
if (this.accountNo == null && getId() != null) {
this.accountNo = "DDS" + String.format("%09d", getId());
}
}
public BigDecimal credit(final BigDecimal amount) {
this.currentBalance = this.currentBalance.add(amount);
return this.currentBalance;
}
public BigDecimal debit(final BigDecimal amount) {
this.currentBalance = this.currentBalance.subtract(amount);
return this.currentBalance;
}
/**
* Turns a minor's account over to its holder, now that they have come of age and been identified.
*
* Until this the guardian operates the account and the bank deals with them. After it the customer
* holds it in their own right, so the guardian is kept only as a record of who stood for them
* before - the account is no longer theirs to operate.
*
* Not done on the birthday alone. An eighteen-year-old the bank has not yet identified is somebody
* it cannot pay, so the turnover waits on their own KYC being verified.
*/
public void becomeAdult() {
this.adultMinor = ADULT;
}
public boolean isMinor() {
return MINOR.equalsIgnoreCase(this.adultMinor);
}
private static final String MINOR = "Minor";
private static final String ADULT = "Adult";
public boolean isClosed() {
return STATUS_CLOSED.equals(this.status);
}
/**
* Closes this Party's account as of the given date. The balance must already be zero - a Party
* is settled to zero (a full Withdraw Deposite/payout) before it is closed, not closed with money
* still owed to it.
*/
public void close(final LocalDate closeDate) {
this.status = STATUS_CLOSED;
this.closeDate = closeDate;
}
/**
* Interest Calculation: parks the interest earned since {@code interestDate} in the account's
* accrued-interest figure - the legacy screen's "Int.Calc" - and stamps the date interest is now
* calculated up to. The balance is untouched, because interest is not the customer's to draw on
* until it is posted.
*
* The stamp is what keeps this safe to re-run: a second calculation for the same date finds no
* days left to accrue, so the same interest cannot be earned twice.
*/
public void accrueInterest(final BigDecimal amount, final LocalDate throughDate, final boolean odInterest) {
if (odInterest) {
this.odInterestCalc = this.odInterestCalc.add(amount);
this.odInterestDate = throughDate;
} else {
this.interestCalc = this.interestCalc.add(amount);
this.interestDate = throughDate;
}
}
/** How far the given interest stream has been calculated through - where the next run starts from. */
public LocalDate interestCalculatedThrough(final boolean odInterest) {
return odInterest ? this.odInterestDate : this.interestDate;
}
/** What the given interest stream is holding, waiting to be posted. */
public BigDecimal accruedInterest(final boolean odInterest) {
return odInterest ? this.odInterestCalc : this.interestCalc;
}
/**
* Interest Posting: moves the given amount out of accrued interest and into the balance, and
* returns the new balance.
*
* The amount is the one the run previewed rather than whatever is accrued at this moment, so what
* gets credited is always what was seen and approved. Since it comes off the accrued figure, a
* second posting of the same interest finds nothing left to move.
*/
public BigDecimal postInterest(final BigDecimal amount, final boolean odInterest) {
if (odInterest) {
this.odInterestCalc = this.odInterestCalc.subtract(amount);
} else {
this.interestCalc = this.interestCalc.subtract(amount);
}
this.currentBalance = this.currentBalance.add(amount);
return this.currentBalance;
}
public Long clientId() {
return this.client.getId();
}
public Long agentId() {
return this.agent.getId();
}
public Map<String, Object> update(final JsonCommand command) {
final Map<String, Object> actualChanges = new LinkedHashMap<>(40);
if (command.isChangeInLongParameterNamed("clientId", this.client.getId())) {
actualChanges.put("clientId", command.longValueOfParameterNamed("clientId"));
}
if (command.isChangeInLongParameterNamed("agentId", this.agent.getId())) {
actualChanges.put("agentId", command.longValueOfParameterNamed("agentId"));
}
stringChange(command, actualChanges, "subType", this.subType, v -> this.subType = v);
stringChange(command, actualChanges, "lastAccount", this.lastAccount, v -> this.lastAccount = v);
if (command.isChangeInBooleanParameterNamed("nominal", this.nominal)) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("nominal");
actualChanges.put("nominal", newValue);
this.nominal = newValue;
}
stringChange(command, actualChanges, "aadharNo", this.aadharNo, v -> this.aadharNo = v);
stringChange(command, actualChanges, "partyName", this.partyName, v -> this.partyName = v);
stringChange(command, actualChanges, "partyName1", this.partyName1, v -> this.partyName1 = v);
stringChange(command, actualChanges, "address1", this.address1, v -> this.address1 = v);
stringChange(command, actualChanges, "address2", this.address2, v -> this.address2 = v);
stringChange(command, actualChanges, "address3", this.address3, v -> this.address3 = v);
stringChange(command, actualChanges, "city", this.city, v -> this.city = v);
stringChange(command, actualChanges, "pincode", this.pincode, v -> this.pincode = v);
stringChange(command, actualChanges, "gender", this.gender, v -> this.gender = v);
stringChange(command, actualChanges, "panCardNo", this.panCardNo, v -> this.panCardNo = v);
stringChange(command, actualChanges, "telephone", this.telephone, v -> this.telephone = v);
stringChange(command, actualChanges, "mobileNoSms", this.mobileNoSms, v -> this.mobileNoSms = v);
stringChange(command, actualChanges, "operation", this.operation, v -> this.operation = v);
if (command.isChangeInDateParameterNamed("birthDate", this.birthDate)) {
actualChanges.put("birthDate", command.stringValueOfParameterNamed("birthDate"));
this.birthDate = command.localDateValueOfParameterNamed("birthDate");
}
stringChange(command, actualChanges, "occupation", this.occupation, v -> this.occupation = v);
stringChange(command, actualChanges, "risk", this.risk, v -> this.risk = v);
stringChange(command, actualChanges, "ckycId", this.ckycId, v -> this.ckycId = v);
stringChange(command, actualChanges, "adultMinor", this.adultMinor, v -> this.adultMinor = v);
if (command.isChangeInDateParameterNamed("minorDate", this.minorDate)) {
actualChanges.put("minorDate", command.stringValueOfParameterNamed("minorDate"));
this.minorDate = command.localDateValueOfParameterNamed("minorDate");
}
if (command.isChangeInLongParameterNamed("guardianClientId", this.guardianClientId)) {
final Long newValue = command.longValueOfParameterNamed("guardianClientId");
actualChanges.put("guardianClientId", newValue);
this.guardianClientId = newValue;
}
stringChange(command, actualChanges, "minAccountOpt", this.minAccountOpt, v -> this.minAccountOpt = v);
stringChange(command, actualChanges, "relation", this.relation, v -> this.relation = v);
if (command.isChangeInDateParameterNamed("openingDate", this.openingDate)) {
actualChanges.put("openingDate", command.stringValueOfParameterNamed("openingDate"));
this.openingDate = command.localDateValueOfParameterNamed("openingDate");
}
if (command.isChangeInIntegerParameterNamed("month", this.month)) {
final Integer newValue = command.integerValueOfParameterNamed("month");
actualChanges.put("month", newValue);
this.month = newValue;
}
if (command.isChangeInDateParameterNamed("reopenDate", this.reopenDate)) {
actualChanges.put("reopenDate", command.stringValueOfParameterNamed("reopenDate"));
this.reopenDate = command.localDateValueOfParameterNamed("reopenDate");
}
if (command.isChangeInBigDecimalParameterNamed("depositAmount", this.depositAmount)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("depositAmount");
actualChanges.put("depositAmount", newValue);
this.depositAmount = newValue;
}
if (command.isChangeInBigDecimalParameterNamed("dailyDepositAmount", this.dailyDepositAmount)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("dailyDepositAmount");
actualChanges.put("dailyDepositAmount", newValue);
this.dailyDepositAmount = newValue;
}
if (command.isChangeInIntegerParameterNamed("days", this.days)) {
final Integer newValue = command.integerValueOfParameterNamed("days");
actualChanges.put("days", newValue);
this.days = newValue;
}
// The rate is not edited here: it is the scheme's, taken when the account is opened or moved
// to another scheme - see takeSchemeRate.
stringChange(command, actualChanges, "ledgerNo", this.ledgerNo, v -> this.ledgerNo = v);
stringChange(command, actualChanges, "withdrawType", this.withdrawType, v -> this.withdrawType = v);
if (command.isChangeInBooleanParameterNamed("lien", this.lien)) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("lien");
actualChanges.put("lien", newValue);
this.lien = newValue;
}
stringChange(command, actualChanges, "lienReason", this.lienReason, v -> this.lienReason = v);
// A released account explains nothing, so the reason goes with the hold. Leaving it behind
// would have the screen read "Lien: No, held for Loan #1234" - a sentence that contradicts
// itself, and which somebody will one day read as the account still being held.
//
// Nothing is lost: what it was held for, who released it and when are all on the command the
// release rode in on, which maker-checker keeps.
if (!this.lien && this.lienReason != null) {
actualChanges.put("lienReason", null);
this.lienReason = null;
}
if (command.isChangeInDateParameterNamed("payDate", this.payDate)) {
actualChanges.put("payDate", command.stringValueOfParameterNamed("payDate"));
this.payDate = command.localDateValueOfParameterNamed("payDate");
}
if (command.isChangeInBigDecimalParameterNamed("paymentAmount", this.paymentAmount)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("paymentAmount");
actualChanges.put("paymentAmount", newValue);
this.paymentAmount = newValue;
}
if (command.isChangeInBigDecimalParameterNamed("openingBalance", this.openingBalance)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("openingBalance");
actualChanges.put("openingBalance", newValue);
this.openingBalance = newValue;
}
if (command.isChangeInBooleanParameterNamed("isSmsActive", this.isSmsActive)) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("isSmsActive");
actualChanges.put("isSmsActive", newValue);
this.isSmsActive = newValue;
}
if (command.isChangeInBooleanParameterNamed("isEmailActive", this.isEmailActive)) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("isEmailActive");
actualChanges.put("isEmailActive", newValue);
this.isEmailActive = newValue;
}
if (command.isChangeInDateParameterNamed("interestDate", this.interestDate)) {
actualChanges.put("interestDate", command.stringValueOfParameterNamed("interestDate"));
this.interestDate = command.localDateValueOfParameterNamed("interestDate");
}
stringChange(command, actualChanges, "spDescription", this.spDescription, v -> this.spDescription = v);
stringChange(command, actualChanges, "freezeCode", this.freezeCode, v -> this.freezeCode = v);
stringChange(command, actualChanges, "advCode", this.advCode, v -> this.advCode = v);
stringChange(command, actualChanges, "creditToBranch", this.creditToBranch, v -> this.creditToBranch = v);
stringChange(command, actualChanges, "creditToAccountType", this.creditToAccountType, v -> this.creditToAccountType = v);
stringChange(command, actualChanges, "creditToAccount", this.creditToAccount, v -> this.creditToAccount = v);
if (command.isChangeInBooleanParameterNamed("nomineeApplicable", this.nomineeApplicable)) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("nomineeApplicable");
actualChanges.put("nomineeApplicable", newValue);
this.nomineeApplicable = newValue;
}
stringChange(command, actualChanges, "nomineeSplitMode", this.nomineeSplitMode, v -> this.nomineeSplitMode = v);
// Correcting a term, an instalment or the rate has to move the maturity figures with it -
// otherwise the account keeps quoting what it was worth under the old terms.
final LocalDate maturityDateBefore = this.maturityDate;
final BigDecimal maturityAmountBefore = this.maturityAmount;
recalculateTerm();
if (!java.util.Objects.equals(maturityDateBefore, this.maturityDate)) {
actualChanges.put("maturityDate", this.maturityDate);
}
if (maturityAmountBefore == null || this.maturityAmount.compareTo(maturityAmountBefore) != 0) {
actualChanges.put("maturityAmount", this.maturityAmount);
}
return actualChanges;
}
private void stringChange(final JsonCommand command, final Map<String, Object> actualChanges, final String paramName,
final String existingValue, final Consumer<String> setter) {
if (command.isChangeInStringParameterNamed(paramName, existingValue)) {
final String newValue = command.stringValueOfParameterNamed(paramName);
actualChanges.put(paramName, newValue);
setter.accept(newValue);
}
}
}
@@ -0,0 +1,146 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.party.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
/**
* A single nominee recorded against a {@link Party}. Maker-checker on nominee edits is enforced at
* the command layer (the whole Party update goes through the standard maker-checker command flow),
* not on this child row individually.
*
* Audited in its own right rather than only through the party: this row decides who receives a
* customer's money after them, and "the party changed at 16:19" does not say that the person named
* to receive it was replaced, nor by whom.
*/
@Getter
@Entity
@Table(name = "m_dds_party_nominee")
public class PartyNominee extends AbstractAuditableCustom {
@ManyToOne
@JoinColumn(name = "party_id", nullable = false)
private Party party;
@Column(name = "nomination_date", nullable = false)
private LocalDate nominationDate;
@Column(name = "name", length = 100, nullable = false)
private String name;
@Column(name = "mobile_no", length = 20)
private String mobileNo;
@Column(name = "birth_date")
private LocalDate birthDate;
/**
* Whether this nominee was under age when nominated, worked out from their date of birth rather
* than asked for.
*
* Asked for, it was got wrong: a ten-year-old was recorded as not a minor, and so no guardian
* was required for them. The date of birth is captured either way and already holds the answer,
* so the clerk is not asked a second time in a form that can disagree with the first.
*
* Kept as a stored column rather than worked out on reading, because it is the age at the
* moment of nomination that the record is of - a nominee who has since come of age was still a
* minor on the day they were named, and the guardian named beside them belongs to that day.
*/
@Column(name = "minor", nullable = false)
private boolean minor;
/** India's age of majority, which is what makes a nominee one who needs a guardian named. */
private static final int AGE_OF_MAJORITY = 18;
@Column(name = "relation", length = 40, nullable = false)
private String relation;
@Column(name = "gender", length = 20, nullable = false)
private String gender;
@Column(name = "guardian_name", length = 100)
private String guardianName;
/**
* What this nominee is entitled to, under a Percentage split. Null under a Sequence split, where
* the order the nominees are held in is the entitlement and there is no share to state.
*/
@Column(name = "share_percent", precision = 5, scale = 2)
private BigDecimal sharePercent;
/**
* Where this nominee stands in the order the bank pays, under a Sequence split. Null under a
* Percentage split, where everyone is paid at once and there is no first.
*/
@Column(name = "sequence_no")
private Integer sequenceNo;
protected PartyNominee() {
//
}
public PartyNominee(final LocalDate nominationDate, final String name, final String mobileNo, final LocalDate birthDate,
final String relation, final String gender, final String guardianName, final BigDecimal sharePercent,
final Integer sequenceNo) {
this.nominationDate = nominationDate;
// Trimmed on the way in. A relation typed as "friend " is a different value from "friend"
// to everything that groups or matches on it, and the trailing space is invisible on screen
// - so the two read as the same relation everywhere except where it counts.
this.name = StringUtils.trimToNull(name);
this.mobileNo = StringUtils.trimToNull(mobileNo);
this.birthDate = birthDate;
this.minor = isUnderAgeOn(birthDate, nominationDate);
this.relation = StringUtils.trimToNull(relation);
this.gender = StringUtils.trimToNull(gender);
this.guardianName = StringUtils.trimToNull(guardianName);
this.sharePercent = sharePercent;
this.sequenceNo = sequenceNo;
}
void setParty(final Party party) {
this.party = party;
}
/**
* Whether someone born on the given day was still under age on the day they were nominated.
*
* Measured against the nomination date rather than today, so a nomination reads the same in ten
* years as it did when it was made - and a nominee who has since come of age does not quietly
* lose the guardian who was named for them at the time.
*
* A missing date of birth is treated as not a minor: the field is required, so a null here can
* only be a record made before it was, and guessing "minor" would demand a guardian for someone
* who may well be eighty.
*/
public static boolean isUnderAgeOn(final LocalDate birthDate, final LocalDate nominationDate) {
if (birthDate == null || nominationDate == null) {
return false;
}
return birthDate.isAfter(nominationDate.minusYears(AGE_OF_MAJORITY));
}
}
@@ -0,0 +1,32 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.party.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when a Close Party command, or a Party Tran collection, targets a Party that is already
* closed.
*/
public class PartyAlreadyClosedException extends AbstractPlatformDomainRuleException {
public PartyAlreadyClosedException(final String partyCode) {
super("error.msg.party.already.closed", "Party " + partyCode + " is already closed", partyCode);
}
}
@@ -0,0 +1,32 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.party.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when a Close Party command is attempted while the Party's balance is still non-zero - the
* remaining balance must be paid out (a Withdraw Deposite to zero) before the account can be closed.
*/
public class PartyHasBalanceException extends AbstractPlatformDomainRuleException {
public PartyHasBalanceException(final String partyCode) {
super("error.msg.party.has.balance", "Party " + partyCode + " still has a non-zero balance and cannot be closed", partyCode);
}
}
@@ -0,0 +1,40 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.party.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException;
/**
* A {@link RuntimeException} thrown when a DDS party resource is not found.
*/
public class PartyNotFoundException extends AbstractPlatformResourceNotFoundException {
public PartyNotFoundException(final Long id) {
super("error.msg.party.id.invalid", "Party with identifier " + id + " does not exist", id);
}
public PartyNotFoundException(final Long id, final EmptyResultDataAccessException e) {
super("error.msg.party.id.invalid", "Party with identifier " + id + " does not exist", id, e);
}
public PartyNotFoundException(final String partyCode) {
super("error.msg.party.code.invalid", "Party with code " + partyCode + " does not exist", partyCode);
}
}
@@ -0,0 +1,33 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.party.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when the savingsAccountNo given for a Party belongs to a different client than the Party's
* own clientId - a Party can only be linked to its own client's savings account.
*/
public class PartySavingsAccountClientMismatchException extends AbstractPlatformDomainRuleException {
public PartySavingsAccountClientMismatchException(final String savingsAccountNo) {
super("error.msg.party.savings.account.client.mismatch",
"Savings account " + savingsAccountNo + " does not belong to this party's client", savingsAccountNo);
}
}
@@ -0,0 +1,32 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.party.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
/**
* Thrown when a Party's savingsAccountNo does not match any non-closed Savings Account.
*/
public class PartySavingsAccountNotFoundException extends AbstractPlatformResourceNotFoundException {
public PartySavingsAccountNotFoundException(final String savingsAccountNo) {
super("error.msg.party.savings.account.not.found", "No open savings account found with account number " + savingsAccountNo,
savingsAccountNo);
}
}
@@ -0,0 +1,38 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.party.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when a Party's subType is not the code of an active DDS Account Type.
*
* The Party screen's Sub Type is chosen from the DDS Account Type master, so an unknown code means
* the caller invented one, and a deactivated code means the scheme has been retired - existing
* parties on it keep running, but no new party may be opened under it.
*/
public class PartySubTypeNotActiveException extends AbstractPlatformDomainRuleException {
public PartySubTypeNotActiveException(final String subType, final boolean exists) {
super(exists ? "error.msg.party.sub.type.not.active" : "error.msg.party.sub.type.invalid",
exists ? "Account type " + subType + " has been deactivated, so no new party can be opened under it"
: "There is no DDS account type with the code " + subType,
subType);
}
}
@@ -0,0 +1,45 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.report.data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import lombok.Builder;
import lombok.Getter;
/**
* A generic tabular result shape shared by every DDS report - one column list plus one row list, both
* keyed the same way, so a single report viewer screen can render any of them without a bespoke
* result type per report.
*/
@Getter
@Builder
public final class DdsReportData implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private final String reportKey;
private final boolean available;
private final String message;
private final List<String> columns;
private final List<Map<String, Object>> rows;
}
@@ -0,0 +1,54 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.transaction.data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public final class PartyTransactionData implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private final Long id;
private final Long partyId;
private final String partyCode;
private final String partyName;
private final Long agentId;
private final String agentCode;
private final LocalDate transactionDate;
private final String transactionType;
private final String direction;
private final String transactionMode;
private final String accountType;
private final String accountNo;
private final BigDecimal amount;
private final String chequeNo;
private final String tokenNo;
private final String description;
private final BigDecimal balanceAfterTransaction;
/** The DDS voucher this line moved under, e.g. DDS/29092026/0001; empty on lines from before vouchers. */
private final String voucherNo;
}
@@ -0,0 +1,106 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.transaction.domain;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* How money changed hands on a DDS entry: over the counter, by book transfer, or through clearing.
*
* It exists because the mode used to be a free-text field. The screen only ever offered these three,
* but nothing behind the screen said so, and the column filled up with two spellings of the same
* word - `Cash` from one path and `CASH` from another. A cash book that groups by mode then reports
* the same day's takings twice under two headings, which is the kind of discrepancy someone spends
* an afternoon chasing before finding it was never a discrepancy at all.
*
* So the list lives here, once, and both the validator and the write service read it from here. What
* a caller sends is matched without regard to case or surrounding space, because an integration
* sending `cash` is unambiguous and rejecting it would be pedantry; but what gets stored is always
* the label below, so the column only ever holds one spelling of each.
*
* Only entries a person makes carry a mode. Interest, commission and maturity postings are the
* system's own work - no cash, no cheque, nothing to record - and a transfer between two DDS accounts
* moves nothing across the counter either, so those are left null rather than labelled.
*/
public enum DdsTransactionMode {
CASH("Cash"), //
TRANSFER("Transfer"), //
CLEARING("Clearing");
private final String label;
DdsTransactionMode(final String label) {
this.label = label;
}
/** The one spelling that is ever stored or shown for this mode. */
public String getLabel() {
return this.label;
}
/**
* The mode a caller's text names, or null if it names none of them. Case and surrounding space
* are ignored; anything else is a mode this scheme does not have.
*/
public static DdsTransactionMode fromLabel(final String value) {
if (value == null) {
return null;
}
final String trimmed = value.trim();
return Arrays.stream(values()).filter(mode -> mode.label.equalsIgnoreCase(trimmed)).findFirst().orElse(null);
}
/**
* The stored form of what a caller sent: the canonical label when it names a mode, and the value
* untouched when it does not.
*
* Unknown text is passed through rather than blanked because this is called after validation has
* already refused it - a silent blanking here would only hide a bug in the order of the two.
*/
public static String canonicalOrOriginal(final String value) {
final DdsTransactionMode mode = fromLabel(value);
return mode == null ? value : mode.label;
}
/** The labels, in the order the entry screen lists them - for error messages and for callers. */
public static List<String> labels() {
return Arrays.stream(values()).map(DdsTransactionMode::getLabel).toList();
}
/** The labels as a reader sees them in a sentence: "Cash, Transfer, Clearing". */
public static String labelsForMessage() {
return String.join(", ", labels());
}
/**
* The same rule as {@link #fromLabel}, expressed as a regular expression, for the validator to
* apply - it reports a failed pattern with the parameter and message already filled in, which a
* hand-rolled check cannot do as tidily.
*
* Built from the labels rather than written out, so the two ways of asking the same question
* cannot answer it differently: add a mode above and both move together.
*/
public static String acceptedPattern() {
return Arrays.stream(values()).map(mode -> Pattern.quote(mode.label)).collect(Collectors.joining("|", "(?i)\\s*(", ")\\s*"));
}
}
@@ -0,0 +1,122 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.transaction.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.PostPersist;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
import org.apache.fineract.organisation.agent.domain.Agent;
import org.apache.fineract.portfolio.dds.party.domain.Party;
import org.apache.fineract.portfolio.dds.voucher.DdsVoucherScope;
/**
* A single posted entry on the DDS ledger: a Party's day's collection ("Party Tran", direction
* CREDIT), one leg of a "Withdraw Deposite" transfer between two parties (the creditor leg CREDIT,
* the debtor leg DEBIT), or an Agent's settlement of collected cash to its branch ("Agent Tran",
* direction DEBIT - no {@link Party} involved, so {@code party} is null for this type).
* {@code balanceAfterTransaction} holds the Party's balance for Party-linked rows, or the Agent's
* balance for an Agent Tran row. Immutable once posted - like any transaction, it is reversed with
* an opposing entry, not edited, so there is deliberately no update API for this entity.
*/
@Getter
@Entity
@Table(name = "m_dds_party_transaction")
public class PartyTransaction extends AbstractAuditableCustom {
@ManyToOne
@JoinColumn(name = "party_id")
private Party party;
@ManyToOne
@JoinColumn(name = "agent_id", nullable = false)
private Agent agent;
@Column(name = "transaction_date", nullable = false)
private LocalDate transactionDate;
@Column(name = "transaction_type", length = 30, nullable = false)
private String transactionType;
@Column(name = "direction", length = 10, nullable = false)
private String direction;
@Column(name = "transaction_mode", length = 20)
private String transactionMode;
@Column(name = "account_type", length = 100)
private String accountType;
@Column(name = "account_no", length = 50)
private String accountNo;
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
private BigDecimal amount;
@Column(name = "cheque_no", length = 40)
private String chequeNo;
@Column(name = "token_no", length = 40)
private String tokenNo;
@Column(name = "description", length = 500)
private String description;
@Column(name = "balance_after_transaction", scale = 6, precision = 19, nullable = false)
private BigDecimal balanceAfterTransaction;
protected PartyTransaction() {
//
}
public PartyTransaction(final Party party, final Agent agent, final LocalDate transactionDate, final String transactionType,
final String direction, final String transactionMode, final String accountType, final String accountNo,
final BigDecimal amount, final String chequeNo, final String tokenNo, final String description,
final BigDecimal balanceAfterTransaction) {
this.party = party;
this.agent = agent;
this.transactionDate = transactionDate;
this.transactionType = transactionType;
this.direction = direction;
this.transactionMode = transactionMode;
this.accountType = accountType;
this.accountNo = accountNo;
this.amount = amount;
this.chequeNo = chequeNo;
this.tokenNo = tokenNo;
this.description = description;
this.balanceAfterTransaction = balanceAfterTransaction;
}
/**
* Notes this line against the operation that wrote it, so the operation's voucher number can be
* stamped on it. The voucher number itself is not mapped here: it is written once, by SQL, after
* the voucher is raised, and a mapped field would be written back as null by any later save.
*/
@PostPersist
private void recordInVoucherScope() {
DdsVoucherScope.recordPartyTransaction(getId());
}
}
@@ -0,0 +1,33 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.transaction.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when an Agent Tran settlement amount exceeds the Agent's current (collected but not yet
* settled) balance.
*/
public class InsufficientAgentBalanceException extends AbstractPlatformDomainRuleException {
public InsufficientAgentBalanceException(final String agentCode) {
super("error.msg.agent.insufficient.balance",
"Agent " + agentCode + " does not have sufficient collected balance for this settlement", agentCode);
}
}
@@ -0,0 +1,32 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.transaction.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when a Withdraw Deposite transfer would take the debtor Party's balance below zero.
*/
public class InsufficientPartyBalanceException extends AbstractPlatformDomainRuleException {
public InsufficientPartyBalanceException(final String partyCode) {
super("error.msg.party.insufficient.balance", "Party " + partyCode + " does not have sufficient balance for this transfer",
partyCode);
}
}
@@ -0,0 +1,38 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.transaction.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when a transaction is posted against an agent who does not collect for the party named on
* it.
*
* The balances would still come out right, but the transaction would be filed under the wrong
* agent - and every agent-wise report is built from exactly that. An agent would show collections
* and transfers for customers who are not theirs, and the officer reading the report has no way to
* tell.
*/
public class PartyNotCollectedByAgentException extends AbstractPlatformDomainRuleException {
public PartyNotCollectedByAgentException(final String partyCode, final String agentCode) {
super("error.msg.party.not.collected.by.agent", "Party " + partyCode + " is not collected by agent " + agentCode,
partyCode, agentCode);
}
}
@@ -0,0 +1,32 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.transaction.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException;
/**
* A {@link RuntimeException} thrown when a DDS party transaction resource is not found.
*/
public class PartyTransactionNotFoundException extends AbstractPlatformResourceNotFoundException {
public PartyTransactionNotFoundException(final Long id, final EmptyResultDataAccessException e) {
super("error.msg.partytransaction.id.invalid", "Party transaction with identifier " + id + " does not exist", id, e);
}
}
@@ -0,0 +1,105 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.voucher;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* What one DDS operation moved, gathered while it runs so a single voucher can be raised for it at
* the end.
*
* A payout, an early closure, a posting run or a transfer each writes several ledger legs and
* passbook lines from several places - the interest brought up to date, the recovery, the payout
* itself. The voucher is the slip for the whole operation, not for each leg, so the legs are noted
* here as they are written and turned into one voucher when the operation finishes.
*
* Held per thread, since an operation runs on one thread from start to finish - including a posting
* run handed to a background thread, which opens its own scope there. Outside a scope nothing is
* recorded.
*/
public final class DdsVoucherScope {
/** A voucher side with no ledger leg behind it - a move between two accounts on the same GL. */
public record Line(String entryType, Long glAccountId, String partyName, String accountNo, BigDecimal amount, String description) {}
private static final ThreadLocal<DdsVoucherScope> CURRENT = new ThreadLocal<>();
private final Set<String> ledgerTransactionIds = new LinkedHashSet<>();
private final List<Long> partyTransactionIds = new ArrayList<>();
private final List<Line> extraLines = new ArrayList<>();
private DdsVoucherScope() {}
/** The scope now open on this thread, or null. */
public static DdsVoucherScope current() {
return CURRENT.get();
}
/** Opens a scope on this thread. The caller must {@link #close} it. */
public static DdsVoucherScope open() {
final DdsVoucherScope scope = new DdsVoucherScope();
CURRENT.set(scope);
return scope;
}
public static void close() {
CURRENT.remove();
}
/** Notes a ledger transaction written for the operation in progress. */
public static void recordLedgerTransaction(final String transactionId) {
final DdsVoucherScope scope = CURRENT.get();
if (scope != null && transactionId != null) {
scope.ledgerTransactionIds.add(transactionId);
}
}
/** Notes a passbook line written for the operation in progress, so it can carry the voucher number. */
public static void recordPartyTransaction(final Long partyTransactionId) {
final DdsVoucherScope scope = CURRENT.get();
if (scope != null && partyTransactionId != null) {
scope.partyTransactionIds.add(partyTransactionId);
}
}
/** Adds a voucher side the ledger does not show - see {@link Line}. */
public static void addLine(final Line line) {
final DdsVoucherScope scope = CURRENT.get();
if (scope != null && line != null) {
scope.extraLines.add(line);
}
}
public Set<String> getLedgerTransactionIds() {
return Collections.unmodifiableSet(this.ledgerTransactionIds);
}
public List<Long> getPartyTransactionIds() {
return Collections.unmodifiableList(this.partyTransactionIds);
}
public List<Line> getExtraLines() {
return Collections.unmodifiableList(this.extraLines);
}
}
@@ -31,20 +31,15 @@ import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.command.core.CommandPipeline;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.portfolio.paymenttype.command.PaymentTypeCreateCommand;
import org.apache.fineract.portfolio.paymenttype.command.PaymentTypeDeleteCommand;
import org.apache.fineract.portfolio.paymenttype.command.PaymentTypeUpdateCommand;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateRequest;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateResponse;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteRequest;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteResponse;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateRequest;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateResponse;
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadService;
import org.springframework.stereotype.Component;
@@ -55,7 +50,7 @@ import org.springframework.stereotype.Component;
public class PaymentTypeApiResource {
private final PaymentTypeReadService readPlatformService;
private final CommandPipeline commandPipeline;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final DefaultToApiJsonSerializer<PaymentTypeData> jsonSerializer;
@GET
@@ -79,14 +74,13 @@ public class PaymentTypeApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create a Payment Type", description = "Creates a new Payment type")
public PaymentTypeCreateResponse createPaymentType(PaymentTypeCreateRequest request) {
final var command = new PaymentTypeCreateCommand();
public String createPaymentType(final PaymentTypeCreateRequest request) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().createPaymentType()
.withJson(jsonSerializer.serialize(request)).build();
command.setPayload(request);
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
final Supplier<PaymentTypeCreateResponse> response = commandPipeline.send(command);
return response.get();
return jsonSerializer.serialize(result);
}
@PUT
@@ -94,18 +88,16 @@ public class PaymentTypeApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update a Payment Type", description = "Updates a Payment Type")
public PaymentTypeUpdateResponse updatePaymentType(@PathParam("paymentTypeId") final Long paymentTypeId,
final PaymentTypeUpdateRequest request) {
public String updatePaymentType(@PathParam("paymentTypeId") final Long paymentTypeId, final PaymentTypeUpdateRequest request) {
request.setId(paymentTypeId);
final var command = new PaymentTypeUpdateCommand();
final CommandWrapper commandRequest = new CommandWrapperBuilder().updatePaymentType(paymentTypeId)
.withJson(jsonSerializer.serialize(request)).build();
command.setPayload(request);
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
final Supplier<PaymentTypeUpdateResponse> response = commandPipeline.send(command);
return response.get();
return jsonSerializer.serialize(result);
}
@DELETE
@@ -113,14 +105,12 @@ public class PaymentTypeApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a Payment Type", description = "Deletes payment type")
public PaymentTypeDeleteResponse deleteCode(@PathParam("paymentTypeId") final Long paymentTypeId) {
public String deleteCode(@PathParam("paymentTypeId") final Long paymentTypeId) {
final var command = new PaymentTypeDeleteCommand();
final CommandWrapper commandRequest = new CommandWrapperBuilder().deletePaymentType(paymentTypeId).build();
command.setPayload(PaymentTypeDeleteRequest.builder().id(paymentTypeId).build());
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
final Supplier<PaymentTypeDeleteResponse> response = commandPipeline.send(command);
return response.get();
return jsonSerializer.serialize(result);
}
}
@@ -18,33 +18,50 @@
*/
package org.apache.fineract.portfolio.paymenttype.handler;
import io.github.resilience4j.retry.annotation.Retry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.command.core.CommandHandler;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateRequest;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateResponse;
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeWriteService;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Component
@RequiredArgsConstructor
public class PaymentTypeCreateCommandHandler implements CommandHandler<PaymentTypeCreateRequest, PaymentTypeCreateResponse> {
@Service
@CommandType(entity = "PAYMENTTYPE", action = "CREATE")
public class PaymentTypeCreateCommandHandler implements NewCommandSourceHandler {
private final PaymentTypeWriteService paymentTypeWriteService;
@Retry(name = "commandPaymentTypeCreate", fallbackMethod = "fallback")
@Override
@Transactional
public PaymentTypeCreateResponse handle(Command<PaymentTypeCreateRequest> command) {
return paymentTypeWriteService.createPaymentType(command.getPayload());
@Autowired
public PaymentTypeCreateCommandHandler(final PaymentTypeWriteService paymentTypeWriteService) {
this.paymentTypeWriteService = paymentTypeWriteService;
}
@Transactional
@Override
public PaymentTypeCreateResponse fallback(Command<PaymentTypeCreateRequest> command, Throwable t) {
return CommandHandler.super.fallback(command, t);
public CommandProcessingResult processCommand(final JsonCommand command) {
final Boolean isSystemDefined = command.booleanObjectValueOfParameterNamed("isSystemDefined");
final PaymentTypeCreateRequest request = PaymentTypeCreateRequest.builder()
.name(command.stringValueOfParameterNamed("name"))
.description(command.stringValueOfParameterNamed("description"))
.isCashPayment(command.booleanObjectValueOfParameterNamed("isCashPayment"))
.position(command.longValueOfParameterNamed("position"))
.codeName(command.stringValueOfParameterNamed("codeName"))
.isSystemDefined(isSystemDefined != null ? isSystemDefined : Boolean.FALSE)
.build();
final PaymentTypeCreateResponse response = this.paymentTypeWriteService.createPaymentType(request);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(response.getResourceId()) //
.build();
}
}
@@ -18,33 +18,41 @@
*/
package org.apache.fineract.portfolio.paymenttype.handler;
import io.github.resilience4j.retry.annotation.Retry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.command.core.CommandHandler;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteRequest;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteResponse;
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeWriteService;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Component
@RequiredArgsConstructor
public class PaymentTypeDeleteCommandHandler implements CommandHandler<PaymentTypeDeleteRequest, PaymentTypeDeleteResponse> {
@Service
@CommandType(entity = "PAYMENTTYPE", action = "DELETE")
public class PaymentTypeDeleteCommandHandler implements NewCommandSourceHandler {
private final PaymentTypeWriteService paymentTypeWriteService;
@Retry(name = "commandPaymentTypeDelete", fallbackMethod = "fallback")
@Override
@Transactional
public PaymentTypeDeleteResponse handle(Command<PaymentTypeDeleteRequest> command) {
return paymentTypeWriteService.deletePaymentType(command.getPayload());
@Autowired
public PaymentTypeDeleteCommandHandler(final PaymentTypeWriteService paymentTypeWriteService) {
this.paymentTypeWriteService = paymentTypeWriteService;
}
@Transactional
@Override
public PaymentTypeDeleteResponse fallback(Command<PaymentTypeDeleteRequest> command, Throwable t) {
return CommandHandler.super.fallback(command, t);
public CommandProcessingResult processCommand(final JsonCommand command) {
final PaymentTypeDeleteRequest request = PaymentTypeDeleteRequest.builder().id(command.entityId()).build();
final PaymentTypeDeleteResponse response = this.paymentTypeWriteService.deletePaymentType(request);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(response.getResourceId()) //
.build();
}
}
@@ -18,33 +18,51 @@
*/
package org.apache.fineract.portfolio.paymenttype.handler;
import io.github.resilience4j.retry.annotation.Retry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.command.core.CommandHandler;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateRequest;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateResponse;
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeWriteService;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Component
@RequiredArgsConstructor
public class PaymentTypeUpdateCommandHandler implements CommandHandler<PaymentTypeUpdateRequest, PaymentTypeUpdateResponse> {
@Service
@CommandType(entity = "PAYMENTTYPE", action = "UPDATE")
public class PaymentTypeUpdateCommandHandler implements NewCommandSourceHandler {
private final PaymentTypeWriteService paymentTypeWriteService;
@Retry(name = "commandPaymentTypeUpdate", fallbackMethod = "fallback")
@Override
@Transactional
public PaymentTypeUpdateResponse handle(Command<PaymentTypeUpdateRequest> command) {
return paymentTypeWriteService.updatePaymentType(command.getPayload());
@Autowired
public PaymentTypeUpdateCommandHandler(final PaymentTypeWriteService paymentTypeWriteService) {
this.paymentTypeWriteService = paymentTypeWriteService;
}
@Transactional
@Override
public PaymentTypeUpdateResponse fallback(Command<PaymentTypeUpdateRequest> command, Throwable t) {
return CommandHandler.super.fallback(command, t);
public CommandProcessingResult processCommand(final JsonCommand command) {
final Boolean isSystemDefined = command.booleanObjectValueOfParameterNamed("isSystemDefined");
final PaymentTypeUpdateRequest request = PaymentTypeUpdateRequest.builder()
.id(command.entityId())
.name(command.stringValueOfParameterNamed("name"))
.description(command.stringValueOfParameterNamed("description"))
.isCashPayment(command.booleanObjectValueOfParameterNamed("isCashPayment"))
.position(command.longValueOfParameterNamed("position"))
.codeName(command.stringValueOfParameterNamed("codeName"))
.isSystemDefined(isSystemDefined != null ? isSystemDefined : Boolean.FALSE)
.build();
final PaymentTypeUpdateResponse response = this.paymentTypeWriteService.updatePaymentType(request);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(response.getResourceId()) //
.build();
}
}
@@ -32,7 +32,8 @@ public enum DepositAccountOnClosureType {
TRANSFER_TO_SAVINGS(200, "depositAccountClosureType.transferToSavings"), //
REINVEST_PRINCIPAL_AND_INTEREST(300, "depositAccountClosureType.reinvestPrincipalAndInterest"), //
REINVEST_PRINCIPAL_ONLY(400, "depositAccountClosureType.reinvestPrincipalOnly"), //
PARTIAL_REINVEST(500, "depositAccountClosureType.partialReinvest"); //
PARTIAL_REINVEST(500, "depositAccountClosureType.partialReinvest"), //
TRANSFER_TO_GL(600, "depositAccountClosureType.transferToGl"); //
private final Integer value;
private final String code;
@@ -53,6 +54,7 @@ public enum DepositAccountOnClosureType {
case 300 -> REINVEST_PRINCIPAL_AND_INTEREST;
case 400 -> REINVEST_PRINCIPAL_ONLY;
case 500 -> PARTIAL_REINVEST;
case 600 -> TRANSFER_TO_GL;
default -> INVALID;
};
}
@@ -91,6 +93,17 @@ public enum DepositAccountOnClosureType {
return this.equals(PARTIAL_REINVEST);
}
/**
* Pays the closure proceeds into a GL account the maker picks, instead of a savings account or cash.
* Only the fixed-deposit premature-closure handler implements it -- see
* DepositAccountDomainServiceJpa#handleFDAccountPreMatureClosure; every other closure path rejects it
* in DepositAccountTransactionDataValidator#validateClosing rather than falling through to the
* terminal "pay the balance out in cash" branch.
*/
public boolean isTransferToGl() {
return this.equals(TRANSFER_TO_GL);
}
// TODO: do we really need this?!?
public static Object[] integerValues() {
return Arrays.stream(values()).filter(value -> !INVALID.equals(value)).map(value -> value.value).toList().toArray();
@@ -123,6 +123,7 @@ public final class DepositsApiConstants {
public static final String isSeniorCitizenParamName = "isSeniorCitizen";
public static final String seniorCitizenInterestRateParamName = "seniorCitizenInterestRate";
public static final String tenureInMonthsParamName = "tenureInMonths";
public static final String tenureInDaysParamName = "tenureInDays";
public static final String interestCompoundingPeriodInMonthsParamName = "interestCompoundingPeriodInMonths";
// Preclosure parameters
@@ -142,6 +143,9 @@ public final class DepositsApiConstants {
public static final String inMultiplesOfDepositTermParamName = "inMultiplesOfDepositTerm";
public static final String inMultiplesOfDepositTermTypeIdParamName = "inMultiplesOfDepositTermTypeId";
public static final String inMultiplesOfDepositTermType = "inMultiplesOfDepositTermType";
// Days after maturity a depositor still has to walk in and renew at the original maturity date --
// null/0 means this product keeps the instant, same-night auto-renewal every FD product had before.
public static final String renewalGracePeriodDaysParamName = "renewalGracePeriodDays";
public static final String depositAmountParamName = "depositAmount";
public static final String depositMinAmountParamName = "minDepositAmount";
@@ -208,7 +212,18 @@ public final class DepositsApiConstants {
public static final String onAccountClosureIdParamName = "onAccountClosureId";
public static final String transferDescriptionParamName = "transferDescription";
public static final String toSavingsAccountIdParamName = "toSavingsAccountId";
// Destination GL account for DepositAccountOnClosureType.TRANSFER_TO_GL -- the closure proceeds are
// credited straight to this GL instead of a savings account or cash.
public static final String toGlAccountIdParamName = "toGlAccountId";
public static final String reinvestAmountParamName = "reinvestAmount";
// Lets a maker waive the product's configured pre-closure penalty for one premature closure --
// see FixedDepositAccount#waivePreClosurePenaltyForThisRequest for why this is a transient
// per-request flag rather than a stored account field.
public static final String waivePreClosurePenaltyParamName = "waivePreClosurePenalty";
// Why the deposit was closed before maturity -- see PrematureClosureReasonType. Persisted (unlike
// waivePreClosurePenaltyParamName above) as the RBI audit trail of which closures waived their
// penalty and why.
public static final String prematureClosureReasonIdParamName = "prematureClosureReasonId";
public static final String savingsAccounts = "savingsAccounts";
public static final String maturityInstructionIdParamName = "maturityInstructionId";
public static final String transferToSavingsIdParamName = "transferToSavingsId";
@@ -237,7 +252,7 @@ public final class DepositsApiConstants {
SavingProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(),
SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(),
SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), chartsParamName, SavingsApiConstants.withHoldTaxParamName,
SavingsApiConstants.taxGroupIdParamName));
SavingsApiConstants.taxGroupIdParamName, SavingsApiConstants.otherGlMappingsParamName));
private static final Set<String> PRECLOSURE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(preClosurePenalApplicableParamName, preClosurePenalInterestParamName, preClosurePenalInterestOnTypeIdParamName));
@@ -247,11 +262,13 @@ public final class DepositsApiConstants {
private static final Set<String> DEPOSIT_TERM_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(minDepositTermParamName,
maxDepositTermParamName, minDepositTermTypeIdParamName, maxDepositTermTypeIdParamName, inMultiplesOfDepositTermParamName,
inMultiplesOfDepositTermTypeIdParamName, depositAmountParamName, depositMinAmountParamName, depositMaxAmountParamName));
inMultiplesOfDepositTermTypeIdParamName, depositAmountParamName, depositMinAmountParamName, depositMaxAmountParamName,
renewalGracePeriodDaysParamName));
private static final Set<String> DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(minDepositTermParamName,
maxDepositTermParamName, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTermParamName,
inMultiplesOfDepositTermType, depositAmountParamName, depositMinAmountParamName, depositMaxAmountParamName));
inMultiplesOfDepositTermType, depositAmountParamName, depositMinAmountParamName, depositMaxAmountParamName,
renewalGracePeriodDaysParamName));
private static final Set<String> RECURRING_DETAILS_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(mandatoryRecommendedDepositAmountParamName, isMandatoryDepositParamName, allowWithdrawalParamName,
@@ -373,6 +390,8 @@ public final class DepositsApiConstants {
fixedDepositRequestData.add("paymentChannelToFundSourceMappings");
fixedDepositRequestData.add("feeToIncomeAccountMappings");
fixedDepositRequestData.add("penaltyToIncomeAccountMappings");
fixedDepositRequestData.add(SavingsApiConstants.otherGlMappingsParamName);
fixedDepositRequestData.add("otherGlMappingsMap");
return fixedDepositRequestData;
}
@@ -413,6 +432,10 @@ public final class DepositsApiConstants {
recurringDepositRequestData.add("prematureWithdrawalOption");
recurringDepositRequestData.add("tenureFlexibility");
recurringDepositRequestData.add("lienMarked");
recurringDepositRequestData.add("lienPercentage");
recurringDepositRequestData.add("missedInstallmentGraceDays");
recurringDepositRequestData.add("missedInstallmentPenaltyType");
recurringDepositRequestData.add("missedInstallmentPenaltyAmount");
recurringDepositRequestData.add("withdrawalFeeForTransfers");
recurringDepositRequestData.add("enableLockinPeriod");
recurringDepositRequestData.add("maturityInstructions");
@@ -472,6 +495,10 @@ public final class DepositsApiConstants {
recurringDepositRequestData.add("prematureWithdrawalOption");
recurringDepositRequestData.add("tenureFlexibility");
recurringDepositRequestData.add("lienMarked");
recurringDepositRequestData.add("lienPercentage");
recurringDepositRequestData.add("missedInstallmentGraceDays");
recurringDepositRequestData.add("missedInstallmentPenaltyType");
recurringDepositRequestData.add("missedInstallmentPenaltyAmount");
recurringDepositRequestData.add("maturityInstructions");
recurringDepositRequestData.add("creditMaturityProceeds");
recurringDepositRequestData.add("penaltyGracePeriodMissedInstallments");
@@ -484,6 +511,8 @@ public final class DepositsApiConstants {
recurringDepositRequestData.add("paymentChannelToFundSourceMappings");
recurringDepositRequestData.add("feeToIncomeAccountMappings");
recurringDepositRequestData.add("penaltyToIncomeAccountMappings");
recurringDepositRequestData.add(SavingsApiConstants.otherGlMappingsParamName);
recurringDepositRequestData.add("otherGlMappingsMap");
return recurringDepositRequestData;
}
@@ -511,6 +540,7 @@ public final class DepositsApiConstants {
fixedDepositInterestCalculationData.add(principalAmountParamName);
fixedDepositInterestCalculationData.add(annualInterestRateParamName);
fixedDepositInterestCalculationData.add(tenureInMonthsParamName);
fixedDepositInterestCalculationData.add(tenureInDaysParamName);
fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName);
fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName);
// Exact calendar-based calculation params
@@ -626,6 +656,8 @@ public final class DepositsApiConstants {
recurringDepositRequestData.add(SavingsApiConstants.balanceSwapOptionParamName);
recurringDepositRequestData.add(SavingsApiConstants.msmeParamName);
recurringDepositRequestData.add(SavingsApiConstants.autoDebitFacilityParamName);
// Lien Marked: how much of this deposit may be given as loan, capped by its product's percentage.
recurringDepositRequestData.add("lienPercentage");
recurringDepositRequestData.add(isSeniorCitizenParamName);
recurringDepositRequestData.add(seniorCitizenInterestRateParamName);
return recurringDepositRequestData;
@@ -0,0 +1,100 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings;
import java.util.Locale;
/**
* The deposit scheme a fixed deposit product is sold under, held as {@code m_savings_product.fd_type}.
*
* <p>
* The three schemes differ in where a posting of interest ends up, which is what the interest posting
* report has to show and the posting itself has to honour:
*
* <ul>
* <li>{@link #SINGLE} — short deposits, priced from a slab table in days. Interest stays in the
* deposit and is paid with the principal at maturity.</li>
* <li>{@link #DOUBLE} — deposits of a year or more, priced from a slab table in years. Interest is
* compounded back into the deposit, so the maturity value is principal plus interest.</li>
* <li>{@link #SPECIAL} — the monthly income scheme. Interest is posted monthly and swept out to the
* customer's linked savings account, which is why a special deposit cannot be opened without one;
* the deposit itself matures at its principal, the interest having already been paid away.</li>
* </ul>
*
* <p>
* The database column is a string rather than an enum ordinal, so the names here are the stored
* values and must not be renamed. {@link #fromString(String)} is lenient about case and blanks
* because the column carries a default of SINGLE and older products predate the column entirely.
*/
public enum FixedDepositType {
/** Interest is held in the deposit and paid at maturity. The default for a product that names none. */
SINGLE,
/** Interest is compounded into the deposit; the maturity value includes it. */
DOUBLE,
/** Interest is posted monthly and transferred to the linked savings account. */
SPECIAL;
/**
* The scheme named by a stored {@code fd_type}, defaulting to {@link #SINGLE}.
*
* <p>
* A null, blank or unrecognised value reads as SINGLE: that is the column's own default, and a
* product carrying something else is a misconfiguration that must not stop its customers' interest
* from being reported.
*/
public static FixedDepositType fromString(final String fdType) {
if (fdType == null || fdType.isBlank()) {
return SINGLE;
}
for (final FixedDepositType candidate : values()) {
if (candidate.name().equalsIgnoreCase(fdType.trim())) {
return candidate;
}
}
return SINGLE;
}
public static boolean isValid(final String fdType) {
if (fdType == null || fdType.isBlank()) {
return false;
}
for (final FixedDepositType candidate : values()) {
if (candidate.name().equalsIgnoreCase(fdType.trim())) {
return true;
}
}
return false;
}
/** Whether interest leaves the deposit for the linked savings account each time it is posted. */
public boolean isPaidOutMonthly() {
return this == SPECIAL;
}
/** Whether a posting of interest adds to the deposit's own balance. */
public boolean isRetainedInDeposit() {
return this != SPECIAL;
}
/** What a customer sees: "FD Special", "FD Double", "FD Single". */
public String displayName() {
return "FD " + name().charAt(0) + name().substring(1).toLowerCase(Locale.ENGLISH);
}
}
@@ -0,0 +1,70 @@
/**
* 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. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings;
import lombok.Getter;
/**
* Why a Fixed/Recurring Deposit was closed before maturity -- captured on every premature closure, not
* just the ones that waive the penalty, so the audit trail always shows a reason rather than only
* recording one when it happens to matter. {@link #isAutoWaived()} is the RBI-driven business rule this
* type exists to encode: a maker cannot leave the platform's own penalty-waiver decision to memory or a
* free-text note for these four reasons -- picking the reason IS the waiver decision, enforced on the
* server regardless of what a client happened to also send for the plain waive-penalty flag.
*/
@Getter
public enum PrematureClosureReasonType {
INVALID(0, "prematureClosureReasonType.invalid", false), //
NORMAL(100, "prematureClosureReasonType.normal", false), //
PREMATURE_EXTENSION(200, "prematureClosureReasonType.prematureExtension", true), //
CONVERSION_TO_ANOTHER_TD(300, "prematureClosureReasonType.conversionToAnotherTd", true), //
DECEASED_SETTLEMENT(400, "prematureClosureReasonType.deceasedSettlement", true), //
NOMINEE_LEGAL_HEIR_CLOSURE(500, "prematureClosureReasonType.nomineeLegalHeirClosure", true); //
private final Integer value;
private final String code;
/** True when this reason, by itself, waives the pre-closure penalty regardless of any manual
* waive-penalty flag also sent -- see the class javadoc. */
private final boolean autoWaived;
PrematureClosureReasonType(final Integer value, final String code, final boolean autoWaived) {
this.value = value;
this.code = code;
this.autoWaived = autoWaived;
}
public static PrematureClosureReasonType fromInt(final Integer v) {
if (v == null) {
return INVALID;
}
return switch (v) {
case 100 -> NORMAL;
case 200 -> PREMATURE_EXTENSION;
case 300 -> CONVERSION_TO_ANOTHER_TD;
case 400 -> DECEASED_SETTLEMENT;
case 500 -> NOMINEE_LEGAL_HEIR_CLOSURE;
default -> INVALID;
};
}
public boolean isInvalid() {
return this.equals(INVALID);
}
}
@@ -278,6 +278,7 @@ public class SavingsApiConstants {
// Additional custom fields
public static final String balanceFrequencyParamName = "balanceFrequency";
public static final String accountClosedZeroBalanceInDayClosingParamName = "accountClosedZeroBalanceInDayClosing";
public static final String productCategoryParamName = "productCategory";
// Current Product — new UI fields
public static final String balanceSwapOptionParamName = "balanceSwapOption";
@@ -286,6 +287,15 @@ public class SavingsApiConstants {
public static final String avgBalanceCalcPeriodParamName = "avgBalanceCalcPeriod";
public static final String validForPeriodParamName = "validForPeriod";
public static final String odAccountClosedZeroBalanceInDayClosingParamName = "odAccountClosedZeroBalanceInDayClosing";
// Overdraft scheme rules a sanction on an account is held to: overdraftLimit is the ceiling (an
// amount), odMinLimit the floor, odSecurityType whether the limit may be clean, secured, or either.
public static final String odMinLimitParamName = "odMinLimit";
public static final String odSecurityTypeParamName = "odSecurityType";
// Excess drawing: whether an account may go past its sanctioned limit, by how much (a percentage of the
// limit), and the penal charge (% p.a.) levied on the excess alone for the days it stays excess.
public static final String odExcessAllowedParamName = "odExcessAllowed";
public static final String odExcessLimitPercentParamName = "odExcessLimitPercent";
public static final String odPenalRateParamName = "odPenalRate";
// Status of Account fields
public static final String accountStatusParamName = "accountStatus";
@@ -234,6 +234,20 @@ public final class SavingsAccountData implements Serializable {
return this.transferInterestToLinkedAccount;
}
/**
* True for a recurring deposit opened under a product whose Interest Calculation Type is "Simple": its interest
* never compounds. Transient because it is only read by the batch interest-posting engine.
*/
private transient boolean simpleInterest;
public void setSimpleInterest(final boolean simpleInterest) {
this.simpleInterest = simpleInterest;
}
public boolean isSimpleInterest() {
return this.simpleInterest;
}
public static SavingsAccountData importInstanceIndividual(Long clientId, Long productId, Long fieldOfficerId, LocalDate submittedOnDate,
BigDecimal nominalAnnualInterestRate, EnumOptionData interestCompoundingPeriodTypeEnum,
EnumOptionData interestPostingPeriodTypeEnum, EnumOptionData interestCalculationTypeEnum,
@@ -144,6 +144,7 @@ public final class SavingsProductData implements Serializable {
@Getter @Setter private Boolean enableLockinPeriod;
@Getter @Setter private String balanceFrequency;
@Getter @Setter private Boolean accountClosedZeroBalanceInDayClosing;
@Getter @Setter private String productCategory;
// New current product fields
@Getter @Setter private Boolean balanceSwapOption;
@@ -152,6 +153,11 @@ public final class SavingsProductData implements Serializable {
@Getter @Setter private String avgBalanceCalcPeriod;
@Getter @Setter private String validForPeriod;
@Getter @Setter private Boolean odAccountClosedZeroBalanceInDayClosing;
@Getter @Setter private BigDecimal odMinLimit;
@Getter @Setter private String odSecurityType;
@Getter @Setter private Boolean odExcessAllowed;
@Getter @Setter private BigDecimal odExcessLimitPercent;
@Getter @Setter private BigDecimal odPenalRate;
// Complex objects wrapped as Maps/Lists so Jackson converts them into the exact JSON your UI expects
@Getter @Setter private Map<String, Object> accountFacilityConfig;
@@ -382,6 +388,11 @@ public final class SavingsProductData implements Serializable {
target.setAvgBalanceCalcPeriod(source.getAvgBalanceCalcPeriod());
target.setValidForPeriod(source.getValidForPeriod());
target.setOdAccountClosedZeroBalanceInDayClosing(source.getOdAccountClosedZeroBalanceInDayClosing());
target.setOdMinLimit(source.getOdMinLimit());
target.setOdSecurityType(source.getOdSecurityType());
target.setOdExcessAllowed(source.getOdExcessAllowed());
target.setOdExcessLimitPercent(source.getOdExcessLimitPercent());
target.setOdPenalRate(source.getOdPenalRate());
target.setOtherGlMappingsMap(source.getOtherGlMappingsMap());
target.setAccountFacilityConfig(source.getAccountFacilityConfig());
target.setAccountFacilityLimits(source.getAccountFacilityLimits());

Some files were not shown because too many files have changed in this diff Show More