Compare commits

..
Author SHA1 Message Date
anshul 33e7aef618 feat: add portfolio investment api constants and domain models
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Waiting to run
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Waiting to run
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Waiting to run
Fineract Documentation build / build (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Waiting to run
Fineract Liquibase Only mode - PostgreSQL / test (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Waiting to run
2026-10-29 13:55:36 +05:30
anshul d5f188258f feat: add investment portfolio module with support for fixed deposits, shares, valuation, and purchase management
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Waiting to run
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Waiting to run
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Waiting to run
Fineract Documentation build / build (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Waiting to run
Fineract Liquibase Only mode - PostgreSQL / test (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Waiting to run
2026-10-28 17:18:48 +05:30
anshul ff772d03b1 feat(investment): maker-checker on all investment actions
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 (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 4 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-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 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-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-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 Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
2026-10-27 17:21:16 +05:30
anshulandClaude Opus 5.5 8db4fc7fee feat(investment): MTM valuation with IDR and mutual fund switch
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 - 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 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 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 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-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 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-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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:03:38 +05:30
anshulandClaude Opus 5.5 9bd1e5306f feat(investment): non-SLR bonds and mutual funds, UAT fixes
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 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-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 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 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 - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (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
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-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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:14:35 +05:30
anshul 140c76dac9 feat(investment): non-SLR rating, 10% limits and debt-only mutual funds
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 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 - PostgreSQL / test (test-core-4) (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 - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Documentation build / build (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 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-23 12:40:43 +05:30
anshul 13ad567a8b feat(investment): bucket wise investment report by time left to maturity 2026-10-23 12:39:53 +05:30
anshul c3bb771d8a feat(investment): total interest received and receivable report 2026-10-23 11:26:09 +05:30
anshul 52ca2bfc03 Merge remote-tracking branch 'origin/dev-latest' into feature/investment
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (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 - 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 (progressive-loan, build-progressive-loan) (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-3) (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 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 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
# Conflicts:
#	fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml
2026-10-22 18:32:44 +05:30
anshul 3ef5a684ae feat(investment): due date diary and keep dropped deals so their numbers are not reused
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 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-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 - 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 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 Cargo & Unit- & Integration tests - MariaDB / 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 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 7 of 10) (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (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 Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
2026-10-22 17:34:41 +05:30
anshul 9b1e381e07 feat(investment): bank-wise TDS on FD interest, GOI interest to the paisa and FD preview fixes
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 Kafka (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (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
2026-10-20 16:16:06 +05:30
anshul 4522f1305d feat(investment): FD with other banks - placement, maturity, premature, renewal, interest and RBI limits
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 - 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-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / 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 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-16 17:12:40 +05:30
anshul ac563badaa fix(investment): pre-merge review fixes for script terms, write-off limit, amortisation and concurrent posting
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 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-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 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 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-3) (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-16 12:11:47 +05:30
anshul 861e2e196e feat(investment): sell, redemption, write-off, GOI transfer and premium amortisation
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 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 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-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-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
2026-10-15 18:44:52 +05:30
anshul 8ad90f0b00 feat(investment): posted interest in transaction log
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 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-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-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 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
2026-10-14 17:40:06 +05:30
anshul 5606b88fbc feat(investment): post interest vouchers, due reminders on home and day-end close preview
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 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 3 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 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-14 15:12:07 +05:30
anshulandClaude Opus 5.5 4bb71382a1 Merge origin/dev-latest into feature/investment
Bring in the latest team changes (FD interest posting approval, recurring
deposit rules, tenant setup, cash exchange PAN, and others).
final-changelog-tenant.xml conflicted because both sides appended
includes: keep the team's includes first, then the investment
0593-0597 includes. File names differ, so the changesets do not clash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:44:29 +05:30
anshulandClaude Opus 5.5 859cabd07d feat(investment): settle purchases to post them to GL on the settlement date
Settlement-date accounting (RBI UCB Investment Directions, confirmed by
Jitendra Sir): a saved purchase is PENDING_SETTLEMENT with nothing in GL.
On or after its settlement date, POST /investment/purchases/{id}/settle
posts the journal entry dated the settlement date at Head Office - Dr
investment GL (book value), Dr 500606 broken-period interest, Dr charge
GLs, Cr the paying bank - through the journal entry command, which also
raises the JV voucher. The deal becomes ACTIVE and keeps the GL
transaction id and settlement day. Settle goes through maker-checker; a
settled deal can no longer be dropped.

Broken-period interest is now counted up to the settlement date instead
of the deal date, and the interest schedule starts from it.

Migration 0597 adds the settlement columns and the SETTLE permission.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:28:09 +05:30
anshulandClaude Opus 5.5 6380e0d7d2 feat(investment): broken-period interest as an expense in the interest schedule
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 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 - PostgreSQL / 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-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
RBI UCB Investment Directions 2025, para 152: broken-period interest paid
to the seller on G-Sec / approved securities is not part of cost but an
expense (confirmed by Jitendra Sir; GL 500606). The interest schedule
therefore no longer carries it as a receivable to be cleared by the first
coupon: the first coupon is income in full. Tests updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:27:07 +05:30
anshulandClaude Opus 5.5 4ad508ee96 feat(investment): Daily Script Master backend
Market rate of each script for a day (open / high / low / close), as the
bank's existing CBS keeps it, so Purchase Entry can take its rate from
here instead of the dealer typing it (Jitendra Sir).

- m_investment_daily_rate, one row per script per day (migration 0596),
  with READ/CREATE/DELETE permissions granted to Investment menu roles.
- /v1/investment/daily-rates: list (optionally per script), save, drop,
  and /latest for the newest rate of a script on or before a date.
- Close rate is mandatory; high may not be below low, and open / close
  must sit inside low-high, with readable messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:27:07 +05:30
anshulandClaude Opus 5.5 6eb562548c feat(investment): keep purchase charges out of book value
Brokerage, stamp duty and the new other charges are expenses of the deal
(Jitendra Sir): they are stored on the purchase and added to the
settlement amount, but no longer to the book value, for securities and
mutual funds alike. Settlement = book value + accrued interest + charges.

Adds m_investment_purchase.other_charges (migration 0595) and the
otherCharges request/response field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:54:59 +05:30
anshulandClaude Opus 5.5 41aeb2b89a Merge origin/dev-latest into feature/investment
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 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 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 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
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Bring in the latest team changes (TDS, shares, cashier, RBI reserve).
final-changelog-tenant.xml conflicted because both sides appended after
0592: keep the team's 0592/0593 TDS includes first, then the investment
0593/0594 includes. File names differ, so the changesets do not clash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:22:55 +05:30
anshulandClaude Opus 5.5 2569e0f836 feat(investment): interest schedule API for Interest 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 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 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 ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Add GET /v1/investment/purchases/{id}/interest-schedule. For a holding it
lists every half-yearly coupon and every 31 Mar / 30 Sep closing provision
from purchase to maturity, worked out on the face value still held:
provision = interest earned since the last coupon (30/360), and the coupon
clears what is already booked (purchased accrued interest or the earlier
provision). Lines up to the as-on date are marked due. Calculation only;
nothing is posted.

The day-count and interest arithmetic move into InvestmentInterestCalculator,
shared with Purchase Entry, and are covered by unit tests using Jitendra
Sir's worked example. Duplicate-code errors on the masters now keep the
original exception as the cause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 18:45:41 +05:30
anshulandClaude Opus 5.5 81b5850b62 feat(investment): Bank/Party Master, Script Master and Purchase Entry backend
Treasury Investment module, first slice, following the bank's existing CBS
Investment screens:

- Bank / Party Master (m_investment_party): auto party code PTY-nnn, address,
  bank type, GL A/c, RBI code, exposure limit, LEI, counterparty bank details.
- Script Master (m_investment_script): auto script code by bond type
  (G00001, B00001, ...), SLR type (GOI / STATE_GOVT / TREASURY / NON_SLR),
  GL A/c, ISIN, interest rate, issue / maturity dates, issuer details.
- Purchase Entry (m_investment_purchase): one deal = one holding. FY deal
  number (e.g. 202627000001), auto cert no, settlement date from T+n.
  Units, book value, premium/discount, accrued interest (30/360 up to the
  deal date), settlement amount and YTM are computed by the backend.

APIs: /v1/investment/parties, /v1/investment/scripts, /v1/investment/purchases,
all through the command pipeline. Permissions READ/CREATE/UPDATE/DELETE plus
_CHECKER rows are seeded with maker-checker off and granted to roles that
hold MENU_INVESTMENT. Mapped GLs must be enabled ASSET detail accounts.
No GL entry is posted by a purchase yet.

Migrations: 0593_investment_masters, 0594_investment_purchase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 12:35:43 +05:30
211 changed files with 16577 additions and 2538 deletions
@@ -169,12 +169,26 @@ public class CommandSourceService {
|| "RECURRINGDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|| "USER".equalsIgnoreCase(commandSource.getEntityName())
|| "ROLE".equalsIgnoreCase(commandSource.getEntityName())
|| isDdsEntity(commandSource.getEntityName())) {
|| isDdsEntity(commandSource.getEntityName())
|| isInvestmentEntity(commandSource.getEntityName())) {
return false;
}
return true;
}
/**
* The Investment module's maker-checker entities. Deals, settlements and their vouchers are entered by one person and
* approved by another, so a super user's investment work goes to a checker as well. Unlike DDS, a super user may still
* check an entry they made themselves. Daily rates are not here: they are entered without approval.
*/
public static boolean isInvestmentEntity(final String entityName) {
return entityName != null && switch (entityName.toUpperCase()) {
case "INVESTMENTPARTY", "INVESTMENTSCRIPT", "INVESTMENTPURCHASE", "INVESTMENTSALE", "INVESTMENTFD", "INVESTMENTTRANSFER",
"INVESTMENTINTEREST", "INVESTMENTVALUATION" -> true;
default -> false;
};
}
/**
* 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.
@@ -95,6 +95,222 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder createInvestmentParty() {
this.actionName = "CREATE";
this.entityName = "INVESTMENTPARTY";
this.entityId = null;
this.href = "/investment/parties";
return this;
}
public CommandWrapperBuilder updateInvestmentParty(final Long partyId) {
this.actionName = "UPDATE";
this.entityName = "INVESTMENTPARTY";
this.entityId = partyId;
this.href = "/investment/parties/" + partyId;
return this;
}
public CommandWrapperBuilder deleteInvestmentParty(final Long partyId) {
this.actionName = "DELETE";
this.entityName = "INVESTMENTPARTY";
this.entityId = partyId;
this.href = "/investment/parties/" + partyId;
return this;
}
public CommandWrapperBuilder createInvestmentScript() {
this.actionName = "CREATE";
this.entityName = "INVESTMENTSCRIPT";
this.entityId = null;
this.href = "/investment/scripts";
return this;
}
public CommandWrapperBuilder updateInvestmentScript(final Long scriptId) {
this.actionName = "UPDATE";
this.entityName = "INVESTMENTSCRIPT";
this.entityId = scriptId;
this.href = "/investment/scripts/" + scriptId;
return this;
}
public CommandWrapperBuilder createInvestmentPurchase() {
this.actionName = "CREATE";
this.entityName = "INVESTMENTPURCHASE";
this.entityId = null;
this.href = "/investment/purchases";
return this;
}
public CommandWrapperBuilder deleteInvestmentPurchase(final Long purchaseId) {
this.actionName = "DELETE";
this.entityName = "INVESTMENTPURCHASE";
this.entityId = purchaseId;
this.href = "/investment/purchases/" + purchaseId;
return this;
}
public CommandWrapperBuilder settleInvestmentPurchase(final Long purchaseId) {
this.actionName = "SETTLE";
this.entityName = "INVESTMENTPURCHASE";
this.entityId = purchaseId;
this.href = "/investment/purchases/" + purchaseId + "/settle";
return this;
}
public CommandWrapperBuilder createInvestmentSale() {
this.actionName = "CREATE";
this.entityName = "INVESTMENTSALE";
this.entityId = null;
this.href = "/investment/sales";
return this;
}
public CommandWrapperBuilder settleInvestmentSale(final Long saleId) {
this.actionName = "SETTLE";
this.entityName = "INVESTMENTSALE";
this.entityId = saleId;
this.href = "/investment/sales/" + saleId + "/settle";
return this;
}
public CommandWrapperBuilder deleteInvestmentSale(final Long saleId) {
this.actionName = "DELETE";
this.entityName = "INVESTMENTSALE";
this.entityId = saleId;
this.href = "/investment/sales/" + saleId;
return this;
}
public CommandWrapperBuilder createInvestmentFd() {
this.actionName = "CREATE";
this.entityName = "INVESTMENTFD";
this.entityId = null;
this.href = "/investment/fds";
return this;
}
public CommandWrapperBuilder matureInvestmentFd(final Long fdId) {
this.actionName = "MATURE";
this.entityName = "INVESTMENTFD";
this.entityId = fdId;
this.href = "/investment/fds/" + fdId + "/mature";
return this;
}
public CommandWrapperBuilder prematureInvestmentFd(final Long fdId) {
this.actionName = "PREMATURE";
this.entityName = "INVESTMENTFD";
this.entityId = fdId;
this.href = "/investment/fds/" + fdId + "/premature";
return this;
}
public CommandWrapperBuilder renewInvestmentFd(final Long fdId) {
this.actionName = "RENEW";
this.entityName = "INVESTMENTFD";
this.entityId = fdId;
this.href = "/investment/fds/" + fdId + "/renew";
return this;
}
public CommandWrapperBuilder postInvestmentFdInterest(final Long fdId) {
this.actionName = "POSTINTEREST";
this.entityName = "INVESTMENTFD";
this.entityId = fdId;
this.href = "/investment/fds/" + fdId + "/interest";
return this;
}
public CommandWrapperBuilder setInvestmentDepositBase() {
this.actionName = "SETBASE";
this.entityName = "INVESTMENTFD";
this.entityId = null;
this.href = "/investment/fds/deposit-base";
return this;
}
public CommandWrapperBuilder amortiseInvestmentPremium() {
this.actionName = "AMORTISE";
this.entityName = "INVESTMENTSALE";
this.entityId = null;
this.href = "/investment/amortisation";
return this;
}
public CommandWrapperBuilder writeOffInvestmentPurchase(final Long purchaseId) {
this.actionName = "WRITEOFF";
this.entityName = "INVESTMENTSALE";
this.entityId = purchaseId;
this.href = "/investment/purchases/" + purchaseId + "/write-off";
return this;
}
public CommandWrapperBuilder redeemInvestmentPurchase(final Long purchaseId) {
this.actionName = "REDEEM";
this.entityName = "INVESTMENTSALE";
this.entityId = purchaseId;
this.href = "/investment/purchases/" + purchaseId + "/redeem";
return this;
}
public CommandWrapperBuilder switchInvestmentFund(final Long purchaseId) {
this.actionName = "SWITCH";
this.entityName = "INVESTMENTSALE";
this.entityId = purchaseId;
this.href = "/investment/purchases/" + purchaseId + "/switch";
return this;
}
public CommandWrapperBuilder dividendInvestmentPurchase(final Long purchaseId) {
this.actionName = "DIVIDEND";
this.entityName = "INVESTMENTPURCHASE";
this.entityId = purchaseId;
this.href = "/investment/purchases/" + purchaseId + "/dividends";
return this;
}
public CommandWrapperBuilder createInvestmentValuation() {
this.actionName = "CREATE";
this.entityName = "INVESTMENTVALUATION";
this.entityId = null;
this.href = "/investment/valuations";
return this;
}
public CommandWrapperBuilder transferInvestmentSecurity(final Long purchaseId) {
this.actionName = "CREATE";
this.entityName = "INVESTMENTTRANSFER";
this.entityId = purchaseId;
this.href = "/investment/purchases/" + purchaseId + "/transfer";
return this;
}
public CommandWrapperBuilder postInvestmentInterest(final Long purchaseId) {
this.actionName = "CREATE";
this.entityName = "INVESTMENTINTEREST";
this.entityId = purchaseId;
this.href = "/investment/purchases/" + purchaseId + "/interest-postings";
return this;
}
public CommandWrapperBuilder createInvestmentDailyRate() {
this.actionName = "CREATE";
this.entityName = "INVESTMENTDAILYRATE";
this.entityId = null;
this.href = "/investment/daily-rates";
return this;
}
public CommandWrapperBuilder deleteInvestmentDailyRate(final Long rateId) {
this.actionName = "DELETE";
this.entityName = "INVESTMENTDAILYRATE";
this.entityId = rateId;
this.href = "/investment/daily-rates/" + rateId;
return this;
}
public CommandWrapperBuilder createChequeStockLot() {
this.actionName = "CREATE";
this.entityName = "CHEQUESTOCKLOT";
@@ -28,7 +28,6 @@ import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderBy;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.persistence.Version;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
@@ -62,15 +61,6 @@ import org.apache.fineract.portfolio.client.domain.Client;
@Table(name = "m_dds_agent", uniqueConstraints = { @UniqueConstraint(columnNames = { "agent_code" }, name = "agent_code_UNIQUE") })
public class Agent extends AbstractAuditableCustom {
/**
* Optimistic lock, as a savings account has: two saves of the same row from different requests -
* two collections at once, or a collection during a posting run - cannot both win; the second is
* refused and retried instead of quietly overwriting the first one's balance.
*/
@Version
@Column(name = "version", nullable = false)
private Long version;
@ManyToOne
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@@ -69,16 +69,6 @@ public class CommissionSlab extends AbstractAuditableCustom {
* Corrects this slab, reporting only what actually changed so the audit trail records the
* correction rather than a wholesale rewrite.
*/
/** Moves where this slab starts - used when the slab before it is corrected, so the two still meet. */
public void startAt(final BigDecimal from) {
this.slabFrom = from;
}
/** Moves where this slab ends - used when the slab after it is corrected, so the two still meet. */
public void endAt(final BigDecimal to) {
this.slabTo = to;
}
public Map<String, Object> update(final JsonCommand command) {
final Map<String, Object> actualChanges = new LinkedHashMap<>(4);
@@ -41,6 +41,7 @@ public final class DdsAccountTypeData implements Serializable {
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;
/**
@@ -55,16 +56,6 @@ public final class DdsAccountTypeData implements Serializable {
/** The scheme's annual interest rate, which an account opened on it takes. */
private final BigDecimal interestRate;
private final Boolean active;
/** The product's own ledger accounts (see DdsAccountType), each with its code and name to show it by. */
private final Long depositLiabilityGlId;
private final String depositLiabilityGlCode;
private final String depositLiabilityGlName;
private final Long interestExpenseGlId;
private final String interestExpenseGlCode;
private final String interestExpenseGlName;
private final Long commissionExpenseGlId;
private final String commissionExpenseGlCode;
private final String commissionExpenseGlName;
private final LocalDateTime createdDate;
private final LocalDateTime lastModifiedDate;
}
@@ -60,6 +60,10 @@ public class DdsAccountType extends AbstractAuditableCustom {
@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.
@@ -115,24 +119,6 @@ public class DdsAccountType extends AbstractAuditableCustom {
@Column(name = "is_active", nullable = false)
private boolean active;
/**
* The ledger accounts this product posts through, chosen on its Accounting tab the way a savings
* product chooses its own. DDS posts through these alone - there is no bank-wide account to fall
* back on - so a product missing one stops its postings with a message naming what to set.
*
* What the bank owes this product's customers - DDS Customer Deposits, or the product's own.
*/
@Column(name = "deposit_liability_gl_id")
private Long depositLiabilityGlId;
/** What interest paid to this product's customers costs the bank. */
@Column(name = "interest_expense_gl_id")
private Long interestExpenseGlId;
/** What the agents' commission on collecting for this product costs the bank. */
@Column(name = "commission_expense_gl_id")
private Long commissionExpenseGlId;
protected DdsAccountType() {
//
}
@@ -144,14 +130,14 @@ public class DdsAccountType extends AbstractAuditableCustom {
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");
this.depositLiabilityGlId = command.longValueOfParameterNamed("depositLiabilityGlId");
this.interestExpenseGlId = command.longValueOfParameterNamed("interestExpenseGlId");
this.commissionExpenseGlId = command.longValueOfParameterNamed("commissionExpenseGlId");
// A newly created account type is available for use straight away.
this.active = !command.parameterExists("active") || command.booleanPrimitiveValueOfParameterNamed("active");
}
@@ -226,12 +212,23 @@ public class DdsAccountType extends AbstractAuditableCustom {
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);
@@ -259,23 +256,6 @@ public class DdsAccountType extends AbstractAuditableCustom {
this.interestRate = newValue;
}
// The product's ledger accounts. All three are required; the deserializer refuses a null.
if (command.parameterExists("depositLiabilityGlId")
&& command.isChangeInLongParameterNamed("depositLiabilityGlId", this.depositLiabilityGlId)) {
this.depositLiabilityGlId = command.longValueOfParameterNamed("depositLiabilityGlId");
actualChanges.put("depositLiabilityGlId", this.depositLiabilityGlId);
}
if (command.parameterExists("interestExpenseGlId")
&& command.isChangeInLongParameterNamed("interestExpenseGlId", this.interestExpenseGlId)) {
this.interestExpenseGlId = command.longValueOfParameterNamed("interestExpenseGlId");
actualChanges.put("interestExpenseGlId", this.interestExpenseGlId);
}
if (command.parameterExists("commissionExpenseGlId")
&& command.isChangeInLongParameterNamed("commissionExpenseGlId", this.commissionExpenseGlId)) {
this.commissionExpenseGlId = command.longValueOfParameterNamed("commissionExpenseGlId");
actualChanges.put("commissionExpenseGlId", this.commissionExpenseGlId);
}
if (command.isChangeInBooleanParameterNamed("active", this.active)) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("active");
actualChanges.put("active", newValue);
@@ -25,7 +25,6 @@ import jakarta.persistence.FetchType;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderBy;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
@@ -46,15 +45,6 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
@Table(name = "m_dds_batch_run")
public class BatchRun extends AbstractAuditableCustom {
/**
* Optimistic lock, as a savings account has: two saves of the same row from different requests -
* two collections at once, or a collection during a posting run - cannot both win; the second is
* refused and retried instead of quietly overwriting the first one's balance.
*/
@Version
@Column(name = "version", nullable = false)
private Long version;
@Column(name = "job_type", length = 40, nullable = false)
private String jobType;
@@ -97,14 +87,6 @@ public class BatchRun extends AbstractAuditableCustom {
@Column(name = "rows_posted", nullable = false)
private Integer rowsPosted = 0;
/**
* What the run actually moved, beside totalAmount - what it was generated to move. They differ when
* rows were passed over at posting (the money had already been paid by another run), and the run
* says so rather than reading as though the whole total went through.
*/
@Column(name = "posted_amount", scale = 6, precision = 19)
private BigDecimal postedAmount;
/** 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;
@@ -173,11 +155,6 @@ public class BatchRun extends AbstractAuditableCustom {
this.rowsPosted = posted;
}
public void recordPosted(final int posted, final BigDecimal amount) {
this.rowsPosted = posted;
this.postedAmount = amount;
}
public void markPosted(final LocalDate postedOnDate) {
this.status = STATUS_POSTED;
this.postedOnDate = postedOnDate;
@@ -23,7 +23,6 @@ import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
@@ -50,15 +49,6 @@ import org.apache.fineract.portfolio.dds.party.domain.Party;
@Table(name = "m_dds_collection_entry")
public class CollectionEntry extends AbstractAuditableCustom {
/**
* Optimistic lock, as a savings account has: two saves of the same row from different requests -
* two collections at once, or a collection during a posting run - cannot both win; the second is
* refused and retried instead of quietly overwriting the first one's balance.
*/
@Version
@Column(name = "version", nullable = false)
private Long version;
/** Waiting for a clerk to count the cash behind it. */
public static final String PENDING = "PENDING";
@@ -178,11 +178,5 @@ public final class PartyData implements Serializable {
private final String nomineeSplitMode;
private final String status;
private final LocalDate closeDate;
/** The product by id, the account currency, and who opened (made operative) and closed it - as a savings account shows. */
private final Long accountTypeId;
private final String currencyCode;
private final LocalDate activatedOnDate;
private final String activatedByUsername;
private final String closedByUsername;
private final Collection<PartyNomineeData> nominees;
}
@@ -28,7 +28,6 @@ import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderBy;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.persistence.Version;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
@@ -61,15 +60,6 @@ import org.apache.fineract.portfolio.client.domain.Client;
@Table(name = "m_dds_party", uniqueConstraints = { @UniqueConstraint(columnNames = { "party_code" }, name = "party_code_UNIQUE") })
public class Party extends AbstractAuditableCustom {
/**
* Optimistic lock, as a savings account has: two saves of the same row from different requests -
* two collections at once, or a collection during a posting run - cannot both win; the second is
* refused and retried instead of quietly overwriting the first one's balance.
*/
@Version
@Column(name = "version", nullable = false)
private Long version;
/** 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");
@@ -314,29 +304,6 @@ public class Party extends AbstractAuditableCustom {
@Column(name = "close_date")
private LocalDate closeDate;
/**
* The DDS product the account is on, by id - subType carries its code for the screens, but a code
* can be renamed and an id cannot, so this is what ties the account to its product's terms and
* ledger accounts (as m_savings_account.product_id does).
*/
@Column(name = "account_type_id")
private Long accountTypeId;
/** The account's currency, as a savings account records its own - set from the bank's currency on opening. */
@Column(name = "currency_code", length = 3)
private String currencyCode;
/** When the account became operative, and who made it so - the checker, where the opening was approved. */
@Column(name = "activatedon_date")
private LocalDate activatedOnDate;
@Column(name = "activatedon_userid")
private Long activatedByUserId;
/** Who closed the account, beside closeDate - when it was closed. */
@Column(name = "closedon_userid")
private Long closedByUserId;
@OneToMany(mappedBy = "party", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@OrderBy("id")
private List<PartyNominee> nominees = new ArrayList<>();
@@ -525,13 +492,10 @@ public class Party extends AbstractAuditableCustom {
this.savingsAccountId = savingsAccountId;
}
/**
* Numbers the account from the DDS account serial (m_dds_account_number_serial), not from its id: an
* id is spent by every save that rolls back, and the bank's account numbers must run without gaps.
*/
public void assignAccountNo(final long serial) {
if (this.accountNo == null) {
this.accountNo = "DDS" + String.format("%09d", serial);
/** 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());
}
}
@@ -575,22 +539,9 @@ public class Party extends AbstractAuditableCustom {
* 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, final Long closedBy) {
public void close(final LocalDate closeDate) {
this.status = STATUS_CLOSED;
this.closeDate = closeDate;
this.closedByUserId = closedBy;
}
/** Ties the account to its product by id; called whenever subType is set or changed. */
public void linkProduct(final Long productId) {
this.accountTypeId = productId;
}
/** Stamps the opening: the account's currency, and the day and user that made it operative. */
public void recordActivation(final String currency, final LocalDate on, final Long by) {
this.currencyCode = currency;
this.activatedOnDate = on;
this.activatedByUserId = by;
}
/**
@@ -22,7 +22,6 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.Builder;
import lombok.Getter;
@@ -52,15 +51,4 @@ public final class PartyTransactionData implements Serializable {
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;
/** The branch the line is booked at, the business day it was recorded, and the journal it was booked under. */
private final Long officeId;
private final String officeName;
private final LocalDate submittedOnDate;
private final String journalTransactionId;
/** The line's own unique reference (m_dds_party_transaction.ref_no). */
private final String refNo;
/** Who keyed the line and when, as a savings transaction shows it. */
private final String createdByUsername;
private final LocalDateTime createdDate;
private final String agentName;
}
@@ -23,14 +23,11 @@ import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.PostPersist;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.UUID;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
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;
@@ -90,36 +87,6 @@ public class PartyTransaction extends AbstractAuditableCustom {
@Column(name = "balance_after_transaction", scale = 6, precision = 19, nullable = false)
private BigDecimal balanceAfterTransaction;
/** The branch the line is booked at - the party's, or its agent's for a line with no party - as savings rows carry. */
@Column(name = "office_id")
private Long officeId;
/**
* The business day the line was recorded, beside transactionDate - the day the money moved. They
* differ when a collection is applied from a cashier's receipt days after it was collected.
*/
@Column(name = "submitted_on_date")
private LocalDate submittedOnDate;
/**
* The acc_gl_journal_entry.transaction_id the line was booked under, so a passbook line and its
* journal can be found from each other. Null for a line that moves nothing on the books (a transfer
* between two customers owed on the same account).
*/
@Column(name = "journal_transaction_id", length = 100)
private String journalTransactionId;
/**
* The line's own reference, unique across DDS, as a savings transaction carries ref_no: what a
* statement, a complaint or a reconciliation quotes to name exactly one line.
*/
@Column(name = "ref_no", length = 40, nullable = false, unique = true)
private String refNo;
/** The voucher the line was printed on - stamped by DdsVoucherService once the operation's voucher is raised. */
@Column(name = "voucher_no", length = 30, insertable = false, updatable = false)
private String voucherNo;
protected PartyTransaction() {
//
}
@@ -141,31 +108,6 @@ public class PartyTransaction extends AbstractAuditableCustom {
this.tokenNo = tokenNo;
this.description = description;
this.balanceAfterTransaction = balanceAfterTransaction;
if (party != null && party.getOffice() != null) {
this.officeId = party.getOffice().getId();
} else if (agent != null && agent.getOffice() != null) {
this.officeId = agent.getOffice().getId();
} else if (agent != null && agent.getClient() != null && agent.getClient().getOffice() != null) {
// An agent saved without a branch is still a client of one.
this.officeId = agent.getClient().getOffice().getId();
}
this.submittedOnDate = DateUtils.getBusinessLocalDate();
}
/** Names the journal this line was booked under, where the caller knows it better than the operation's last one. */
public PartyTransaction linkJournal(final String transactionId) {
this.journalTransactionId = transactionId;
return this;
}
@PrePersist
private void linkToJournal() {
if (this.refNo == null) {
this.refNo = "DDST" + UUID.randomUUID().toString().replace("-", "").substring(0, 20).toUpperCase();
}
if (this.journalTransactionId == null) {
this.journalTransactionId = DdsVoucherScope.lastLedgerTransaction();
}
}
/**
@@ -48,8 +48,6 @@ public final class DdsVoucherScope {
private final Set<String> ledgerTransactionIds = new LinkedHashSet<>();
private final List<Long> partyTransactionIds = new ArrayList<>();
private final List<Line> extraLines = new ArrayList<>();
/** The journal most recently written in this operation - what the next passbook line saved belongs to. */
private String lastLedgerTransactionId;
private DdsVoucherScope() {}
@@ -74,24 +72,6 @@ public final class DdsVoucherScope {
final DdsVoucherScope scope = CURRENT.get();
if (scope != null && transactionId != null) {
scope.ledgerTransactionIds.add(transactionId);
scope.lastLedgerTransactionId = transactionId;
}
}
/**
* The journal the passbook line being saved now belongs to: every DDS movement writes its journal
* first and its passbook line straight after, so it is the last one written in this operation.
*/
public static String lastLedgerTransaction() {
final DdsVoucherScope scope = CURRENT.get();
return scope == null ? null : scope.lastLedgerTransactionId;
}
/** For a movement that writes no journal, so its passbook line does not claim the previous one's. */
public static void forgetLastLedgerTransaction() {
final DdsVoucherScope scope = CURRENT.get();
if (scope != null) {
scope.lastLedgerTransactionId = null;
}
}
@@ -303,13 +303,9 @@ public class AgentWritePlatformServiceJpaRepositoryImpl implements AgentWritePla
+ "join m_savings_product p on p.id = a.product_id "
+ "where a.account_no = ? and a.status_enum = 300";
/**
* Whether a savings account is a DDS customer's scheme account - one a DDS party is linked to.
* Read off the parties themselves rather than a configured product id: what makes an account a
* scheme account is that a scheme runs on it, whatever product it was opened under.
*/
private static final String DDS_SCHEME_ACCOUNT_SQL = "select count(*) from m_dds_party p "
+ "join m_savings_account a on a.id = p.savings_account_id where a.account_no = ?";
/** The product DDS opens customers' scheme accounts under - see 0492. */
private static final String DDS_DEPOSIT_PRODUCT_SQL = "select value from c_configuration "
+ "where name = 'dds-deposit-product' and enabled = true";
private static final int SAVINGS_DEPOSIT_TYPE = 100;
@@ -357,8 +353,9 @@ public class AgentWritePlatformServiceJpaRepositoryImpl implements AgentWritePla
// A DDS scheme account is a savings account too, so the check above lets it through - but it
// belongs to a customer's scheme, and commission paid into it would be counted as that
// customer's deposits by every DDS interest run from then on.
final Integer schemes = this.jdbcTemplate.queryForObject(DDS_SCHEME_ACCOUNT_SQL, Integer.class, accountNo); // NOSONAR
if (schemes != null && schemes > 0) {
final Long productId = ((Number) account.get("product_id")).longValue();
final List<Long> ddsProduct = this.jdbcTemplate.queryForList(DDS_DEPOSIT_PRODUCT_SQL, Long.class); // NOSONAR
if (!ddsProduct.isEmpty() && productId.equals(ddsProduct.get(0))) {
throw new AgentSavingsAccountNotOperativeException(accountNo, "DDS scheme");
}
}
@@ -44,8 +44,6 @@ public final class CommissionSlabCommandFromApiJsonDeserializer {
// A List, not a Set: isOneOfTheseStringValues only lowercases both sides in its List overload.
private static final List<String> SUPPORTED_SLAB_TYPES = List.of("ACCOUNT", "COMMISSION");
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList("slabs", "dateFormat", "locale"));
private static final Set<String> SUPPORTED_UPDATE_PARAMETERS = new HashSet<>(
@@ -95,7 +93,7 @@ public final class CommissionSlabCommandFromApiJsonDeserializer {
v.reset().parameterAtIndexArray("slabTo", i).value(extractBigDecimal(slabJson, "slabTo")).notNull()
.zeroOrPositiveAmount();
v.reset().parameterAtIndexArray("commissionPercent", i).value(extractBigDecimal(slabJson, "commissionPercent"))
.notNull().zeroOrPositiveAmount().notGreaterThanMax(HUNDRED);
.notNull().zeroOrPositiveAmount();
}
}
@@ -136,7 +134,7 @@ public final class CommissionSlabCommandFromApiJsonDeserializer {
if (this.fromApiJsonHelper.parameterExists("commissionPercent", element)) {
v.reset().parameter("commissionPercent")
.value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("commissionPercent", element)).notNull()
.zeroOrPositiveAmount().notGreaterThanMax(HUNDRED);
.zeroOrPositiveAmount();
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
@@ -21,21 +21,16 @@ package org.apache.fineract.organisation.commissionslab.service;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import jakarta.persistence.PersistenceException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.organisation.commissionslab.domain.CommissionSlab;
import org.apache.fineract.organisation.commissionslab.domain.CommissionSlabRepository;
import org.apache.fineract.organisation.commissionslab.exception.CommissionSlabNotFoundException;
@@ -73,14 +68,6 @@ public class CommissionSlabWritePlatformServiceJpaRepositoryImpl implements Comm
slabs.add(slab);
}
// The chart each type will have once these are added - what is stored plus this request -
// must still read as one unbroken scale from 0.
for (final String type : slabs.stream().map(CommissionSlab::getSlabType).distinct().toList()) {
final List<CommissionSlab> chart = new ArrayList<>(this.commissionSlabRepository.findByType(type));
chart.addAll(slabs.stream().filter(s -> s.getSlabType().equals(type)).toList());
rejectBrokenChart(type, chart);
}
this.commissionSlabRepository.saveAllAndFlush(slabs);
return new CommandProcessingResultBuilder() //
@@ -106,9 +93,6 @@ public class CommissionSlabWritePlatformServiceJpaRepositoryImpl implements Comm
final CommissionSlab slab = this.commissionSlabRepository.findById(slabId)
.orElseThrow(() -> new CommissionSlabNotFoundException(slabId));
final String oldType = slab.getSlabType();
final BigDecimal oldFrom = slab.getSlabFrom();
final BigDecimal oldTo = slab.getSlabTo();
final Map<String, Object> changes = slab.update(command);
// Re-checked after the change, against every other slab of its type. A correction can just as
@@ -117,38 +101,8 @@ public class CommissionSlabWritePlatformServiceJpaRepositoryImpl implements Comm
if (!changes.isEmpty()) {
final List<CommissionSlab> others = this.commissionSlabRepository.findByType(slab.getSlabType()).stream()
.filter(other -> !other.getId().equals(slab.getId())).toList();
// A boundary moved within the same chart takes its neighbour with it, so the two slabs still
// meet a paisa apart and the chart stays unbroken: 10,000 -> 12,000 on this slab's To moves
// the next slab's From from 10,000.01 to 12,000.01.
final List<CommissionSlab> moved = new ArrayList<>();
if (oldType.equals(slab.getSlabType())) {
for (final CommissionSlab other : others) {
if (slab.getSlabTo().compareTo(oldTo) != 0 && other.getSlabFrom().compareTo(oldTo.add(PAISA)) == 0) {
other.startAt(slab.getSlabTo().add(PAISA));
moved.add(other);
}
if (slab.getSlabFrom().compareTo(oldFrom) != 0 && other.getSlabTo().compareTo(oldFrom.subtract(PAISA)) == 0) {
other.endAt(slab.getSlabFrom().subtract(PAISA));
moved.add(other);
}
}
}
rejectIfOverlapping(slab, others);
final List<CommissionSlab> chart = new ArrayList<>(others);
chart.add(slab);
rejectBrokenChart(slab.getSlabType(), chart);
if (!oldType.equals(slab.getSlabType())) {
// Moved to the other type: the chart it left must still be whole too.
rejectBrokenChart(oldType, this.commissionSlabRepository.findByType(oldType).stream()
.filter(other -> !other.getId().equals(slab.getId())).toList());
}
this.commissionSlabRepository.saveAll(moved);
this.commissionSlabRepository.saveAndFlush(slab);
if (!moved.isEmpty()) {
changes.put("neighboursAdjusted", moved.size());
}
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(slabId).with(changes).build();
@@ -162,63 +116,6 @@ public class CommissionSlabWritePlatformServiceJpaRepositoryImpl implements Comm
* explain it. Ranges are inclusive at both ends, so touching endpoints count as an overlap: a
* slab ending at 50000 and the next starting at 50000 both match exactly 50000.
*/
/** Collections are rupees and paise, so consecutive slabs meet one paisa apart. */
private static final BigDecimal PAISA = new BigDecimal("0.01");
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
/**
* A chart the commission run can always read: every slab From <= To, a percentage of 0 to 100, money
* to the paisa, starting at 0, and each slab beginning exactly a paisa after the one before ends.
*
* The run matches from <= collection <= to and stops with "no commission slab" for an agent whose
* collection no slab covers - so a gap, or a chart starting above 0, is not a gentler setting, it is
* a run that fails. Refused here, when the chart is saved, rather than discovered on payday.
*/
private void rejectBrokenChart(final String type, final List<CommissionSlab> chart) {
final List<CommissionSlab> sorted = chart.stream().sorted(Comparator.comparing(CommissionSlab::getSlabFrom)).toList();
final List<ApiParameterError> errors = new ArrayList<>();
for (int i = 0; i < sorted.size(); i++) {
final CommissionSlab slab = sorted.get(i);
final String range = plain(slab.getSlabFrom()) + " - " + plain(slab.getSlabTo());
if (slab.getSlabFrom().compareTo(slab.getSlabTo()) > 0) {
errors.add(chartError("from.after.to", type + " slab " + range + ": From is more than To.", "slabFrom"));
}
if (slab.getSlabFrom().stripTrailingZeros().scale() > 2 || slab.getSlabTo().stripTrailingZeros().scale() > 2) {
errors.add(chartError("paise", type + " slab " + range + ": amounts can have at most two decimals (paise).", "slabFrom"));
}
if (slab.getCommissionPercent().signum() < 0 || slab.getCommissionPercent().compareTo(HUNDRED) > 0) {
errors.add(chartError("percent", type + " slab " + range + ": the percentage must be from 0 to 100.",
"commissionPercent"));
}
if (i == 0) {
if (slab.getSlabFrom().signum() != 0) {
errors.add(chartError("not.from.zero", type + " slabs must start at 0 - nothing covers 0 to "
+ plain(slab.getSlabFrom().subtract(PAISA)) + ".", "slabFrom"));
}
} else {
final BigDecimal expected = sorted.get(i - 1).getSlabTo().add(PAISA);
if (slab.getSlabFrom().compareTo(expected) > 0) {
errors.add(chartError("gap", type + " slabs leave a gap: nothing covers " + plain(expected) + " to "
+ plain(slab.getSlabFrom().subtract(PAISA)) + ". Start this slab at " + plain(expected) + ".",
"slabFrom"));
}
}
}
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("error.msg.commissionslab.chart.broken",
"The " + type + " commission chart would not cover every collection: " + errors.get(0).getDefaultUserMessage(),
errors);
}
}
private static ApiParameterError chartError(final String code, final String message, final String parameter) {
return ApiParameterError.parameterError("error.msg.commissionslab." + code, message, parameter);
}
private static String plain(final BigDecimal value) {
return value.setScale(2, RoundingMode.HALF_UP).toPlainString();
}
private void rejectIfOverlapping(final CommissionSlab candidate, final List<CommissionSlab> existing) {
for (final CommissionSlab other : existing) {
if (!other.getSlabType().equals(candidate.getSlabType())) {
@@ -29,32 +29,6 @@ public final class ApprovalCounterRegistry {
private static final String DDS_ENTITIES = "'PARTY','AGENT','PARTYTRANSACTION','DDSACCOUNTTYPE','COMMISSIONSLAB',"
+ "'DDSBATCH','DDSCOLLECTIONENTRY'";
/** DDS commands held for a checker, as the DDS approval queue reads them. Aliased {@code c}. */
private static final String DDS_PENDING = "FROM m_portfolio_command_source c JOIN m_permission p "
+ "ON p.code = c.action_name || '_' || c.entity_name AND p.can_maker_checker = true WHERE c.status = 2 ";
/**
* DDS product and DDS account requests, listed under Product Approval and Pending Account with every
* other product and account rather than under DDS Approvals or the Checker Inbox. Written for any
* alias, since the Checker Inbox counts with {@code aud} and the DDS counts with {@code c}.
*/
private static String ddsProductRequest(final String alias) {
return "(" + alias + ".entity_name = 'DDSACCOUNTTYPE' AND " + alias + ".action_name IN ('CREATE','UPDATE'))";
}
private static String ddsAccountRequest(final String alias) {
return "(" + alias + ".entity_name = 'PARTY' AND " + alias + ".action_name IN ('CREATE','UPDATE'))";
}
/**
* A DDS agent request of any kind - opening, editing or closing one. An agent has no voucher and no
* amount, so these wait under their own Agent Approval tab rather than among the money entries of
* DDS Approvals or the Checker Inbox.
*/
private static String ddsAgentRequest(final String alias) {
return "(" + alias + ".entity_name = 'AGENT')";
}
/** Maker-checker commands awaiting a checker, limited to the offices and checker rights the caller has. */
private static final String MC_PENDING = "FROM m_portfolio_command_source aud WHERE aud.status = 2 "
+ "AND (:ho OR EXISTS (SELECT 1 FROM m_office o2 WHERE o2.id = aud.office_id AND o2.hierarchy LIKE :h)) "
@@ -75,9 +49,7 @@ public final class ApprovalCounterRegistry {
// ── Fineract's own maker-checker and pending lists ────────────────────────────
new ApprovalCounter("checker-inbox", "ALL_FUNCTIONS_READ",
"SELECT count(*) " + MC_PENDING + "AND upper(aud.entity_name) NOT IN (" + PRODUCT_ENTITIES + ") "
+ "AND NOT (upper(aud.entity_name) IN (" + STAGED_ACCOUNT_ENTITIES + ") AND upper(aud.action_name) = 'UPDATE') "
+ "AND NOT " + ddsProductRequest("aud") + " AND NOT " + ddsAccountRequest("aud")
+ " AND NOT " + ddsAgentRequest("aud")),
+ "AND NOT (upper(aud.entity_name) IN (" + STAGED_ACCOUNT_ENTITIES + ") AND upper(aud.action_name) = 'UPDATE')"),
new ApprovalCounter("client-approval", "READ_CLIENT",
"SELECT count(*) FROM m_client c WHERE c.status_enum = 100 "
+ "AND EXISTS (SELECT 1 FROM m_office o WHERE o.id = c.office_id AND o.hierarchy LIKE :h)"),
@@ -95,16 +67,14 @@ public final class ApprovalCounterRegistry {
+ "AND COALESCE(s.terminate_true, false) = false AND " + CLIENT_OFFICE
+ ") + (SELECT count(*) FROM m_current_account ca WHERE ca.status_enum = 100 "
+ "AND COALESCE(ca.terminate_true, false) = false "
+ "AND EXISTS (SELECT 1 FROM m_office o WHERE o.id = ca.office_id AND o.hierarchy LIKE :h)) "
+ "+ (SELECT count(*) " + DDS_PENDING + "AND " + ddsAccountRequest("c") + ")"),
+ "AND EXISTS (SELECT 1 FROM m_office o WHERE o.id = ca.office_id AND o.hierarchy LIKE :h))"),
new ApprovalCounter("pending-share-accounts", "ALL_FUNCTIONS_READ",
"SELECT count(*) FROM m_share_account WHERE status_enum = 100"),
new ApprovalCounter("product-approval", "ALL_FUNCTIONS_READ",
"SELECT (SELECT count(*) FROM m_saving_product_temp WHERE status = 'PENDING' "
+ "AND product_type IN ('SAVINGS','CURRENT','SHARE','FD','RD')) + (SELECT count(*) " + MC_PENDING
+ "AND upper(aud.entity_name) IN (" + PRODUCT_ENTITIES + ") "
+ "AND NOT (upper(aud.entity_name) IN (" + STAGED_PRODUCT_ENTITIES + ") AND upper(aud.action_name) = 'UPDATE')) "
+ "+ (SELECT count(*) " + DDS_PENDING + "AND " + ddsProductRequest("c") + ")"),
+ "AND NOT (upper(aud.entity_name) IN (" + STAGED_PRODUCT_ENTITIES + ") AND upper(aud.action_name) = 'UPDATE'))"),
new ApprovalCounter("pending-savings-account-edits", "ALL_FUNCTIONS_READ",
"SELECT (SELECT count(*) FROM m_savings_account_pending_edit WHERE status = 'PENDING') "
+ "+ (SELECT count(*) FROM m_current_account_pending_edit WHERE status = 'PENDING') "
@@ -134,10 +104,9 @@ public final class ApprovalCounterRegistry {
new ApprovalCounter(DISBURSEMENT_REQUESTS, "ALL_FUNCTIONS_READ",
"SELECT count(*) FROM m_clerk_disbursement_request WHERE status = 'PENDING'"),
new ApprovalCounter("dds-approvals", null,
"SELECT count(*) " + DDS_PENDING + "AND c.entity_name IN (" + DDS_ENTITIES + ") "
+ "AND NOT " + ddsProductRequest("c") + " AND NOT " + ddsAccountRequest("c")
+ " AND NOT " + ddsAgentRequest("c")),
new ApprovalCounter("agent-approvals", null, "SELECT count(*) " + DDS_PENDING + "AND " + ddsAgentRequest("c")),
"SELECT count(*) FROM m_portfolio_command_source c JOIN m_permission p "
+ "ON p.code = c.action_name || '_' || c.entity_name AND p.can_maker_checker = true "
+ "WHERE c.status = 2 AND c.entity_name IN (" + DDS_ENTITIES + ")"),
new ApprovalCounter("chequebook-requests", "READ_CHEQUEBOOKPENDINGREQUEST",
"SELECT count(*) FROM m_chequebook_pending_request WHERE status = 'PENDING'"),
new ApprovalCounter("closure-requests", "ALL_FUNCTIONS_READ",
@@ -46,6 +46,8 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.cashreport.service.VaultPositionService;
import org.apache.fineract.portfolio.denominationmaster.service.CountedCashLedger;
import org.apache.fineract.portfolio.investment.service.InvestmentFdInterestService;
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsDailyInterestOrchestrator;
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsInterestPostingReportService;
import org.apache.fineract.portfolio.savings.fdinterest.FixedDepositInterestPostingOrchestrator;
@@ -95,8 +97,16 @@ public class BranchDayRegisterApiResource {
private final SavingsDailyInterestOrchestrator savingsDailyInterestOrchestrator;
private final FixedDepositInterestPostingReportService fdInterestPostingReportService;
private final FixedDepositInterestPostingOrchestrator fdInterestPostingOrchestrator;
private final InvestmentPurchaseReadPlatformService investmentPurchaseReadPlatformService;
private final InvestmentFdInterestService investmentFdInterestService;
private final Gson gson = new Gson();
private boolean isHeadOffice(final Long officeId) {
final Integer topLevel = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_office WHERE id = ? AND parent_id IS NULL",
Integer.class, officeId);
return topLevel != null && topLevel > 0;
}
// ── GET /v1/branchday ── the register itself ──────────────────────────────
/**
@@ -260,6 +270,13 @@ public class BranchDayRegisterApiResource {
final Map<String, Object> fdInterest = fdInterestPostingReportService.dayCloseInterestView(officeId, businessDate);
result.put("fdInterestPending", fdInterest.get("pending"));
result.put("fdInterestDue", fdInterest.get("due"));
// Investment interest vouchers (coupon / provision) whose date has come but which are not posted yet.
// A reminder only: it never stops the close (Jitendra Sir). Investments are booked at Head Office, so
// only the Head Office close shows it.
result.put("investmentInterestPending",
isHeadOffice(officeId) ? investmentPurchaseReadPlatformService.retrieveDueInterest(0) : List.of());
// The same for FDs placed with other banks: interest vouchers and maturities due. A reminder only.
result.put("investmentFdPending", isHeadOffice(officeId) ? investmentFdInterestService.due(0) : List.of());
return gson.toJson(result);
}
@@ -53,16 +53,14 @@ public class CustomerSearchApiResource {
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Search for a customer", description = "AND-combines whichever of clientId/name/mobileNo/accountNumber/aadhaarNumber/panNumber/address/accountTypes are supplied - at least one is required. accountTypes is a comma-separated list (SAVINGS,CURRENT,FIXED_DEPOSIT,RECURRING_DEPOSIT).")
public Collection<CustomerSearchResultData> search(@QueryParam("clientId") final String clientId,
@QueryParam("name") final String name, @QueryParam("mobileNo") final String mobileNo,
@Operation(summary = "Search for a customer", description = "AND-combines whichever of name/accountNumber/aadhaarNumber/panNumber/address/accountTypes are supplied - at least one is required. accountTypes is a comma-separated list (SAVINGS,CURRENT,FIXED_DEPOSIT,RECURRING_DEPOSIT).")
public Collection<CustomerSearchResultData> search(@QueryParam("name") final String name,
@QueryParam("accountNumber") final String accountNumber, @QueryParam("aadhaarNumber") final String aadhaarNumber,
@QueryParam("panNumber") final String panNumber, @QueryParam("address") final String address,
@QueryParam("accountTypes") final String accountTypes) {
final List<String> accountTypeList = accountTypes == null || accountTypes.isBlank() ? List.of()
: Arrays.asList(accountTypes.split(","));
return safely(() -> this.readPlatformService.search(clientId, name, mobileNo, accountNumber, aadhaarNumber, panNumber, address,
accountTypeList));
return safely(() -> this.readPlatformService.search(name, accountNumber, aadhaarNumber, panNumber, address, accountTypeList));
}
/** Surfaces IllegalArgumentException as a clean 400 instead of an opaque 500. */
@@ -27,13 +27,12 @@ public interface CustomerSearchReadPlatformService {
/**
* Multi-field AND search - only non-blank parameters are applied. At least one must be
* supplied. name/address are partial, case-insensitive matches; accountNumber/aadhaarNumber/
* panNumber are exact matches (they're precise identifiers, not free text). clientId is an exact match
* on the client's id and mobileNo a partial match on their mobile number. accountTypes, if
* panNumber are exact matches (they're precise identifiers, not free text). accountTypes, if
* non-empty, restricts results to those account types only (SAVINGS/CURRENT/FIXED_DEPOSIT/
* RECURRING_DEPOSIT) - a client with no account, or only accounts of other types, is excluded
* entirely when this filter is active (unlike the unfiltered case, where a client with no
* account at all still appears once for informational purposes).
*/
Collection<CustomerSearchResultData> search(String clientId, String name, String mobileNo, String accountNumber, String aadhaarNumber,
String panNumber, String address, List<String> accountTypes);
Collection<CustomerSearchResultData> search(String name, String accountNumber, String aadhaarNumber, String panNumber,
String address, List<String> accountTypes);
}
@@ -69,11 +69,8 @@ public class CustomerSearchReadPlatformServiceImpl implements CustomerSearchRead
""";
@Override
public Collection<CustomerSearchResultData> search(final String clientId, final String name, final String mobileNo,
final String accountNumber, final String aadhaarNumber, final String panNumber, final String address,
final List<String> accountTypes) {
final boolean hasClientId = clientId != null && !clientId.isBlank();
final boolean hasMobile = mobileNo != null && !mobileNo.isBlank();
public Collection<CustomerSearchResultData> search(final String name, final String accountNumber, final String aadhaarNumber,
final String panNumber, final String address, final List<String> accountTypes) {
final boolean hasName = name != null && !name.isBlank();
final boolean hasAccountNumber = accountNumber != null && !accountNumber.isBlank();
final boolean hasAadhaar = aadhaarNumber != null && !aadhaarNumber.isBlank();
@@ -82,12 +79,9 @@ public class CustomerSearchReadPlatformServiceImpl implements CustomerSearchRead
final List<String> effectiveAccountTypes = accountTypes == null ? List.of()
: accountTypes.stream().filter(t -> t != null && !t.isBlank()).map(String::trim).map(String::toUpperCase).toList();
if (!hasClientId && !hasMobile && !hasName && !hasAccountNumber && !hasAadhaar && !hasPan && !hasAddress
&& effectiveAccountTypes.isEmpty()) {
throw new IllegalArgumentException(
"Enter at least one search field - client ID, name, mobile number, account number, Aadhaar, PAN, or address.");
if (!hasName && !hasAccountNumber && !hasAadhaar && !hasPan && !hasAddress && effectiveAccountTypes.isEmpty()) {
throw new IllegalArgumentException("Enter at least one search field - name, account number, Aadhaar, PAN, or address.");
}
final Long clientIdValue = hasClientId ? parseClientId(clientId) : null;
for (final String type : effectiveAccountTypes) {
if (!VALID_ACCOUNT_TYPES.contains(type)) {
throw new IllegalArgumentException(
@@ -98,19 +92,10 @@ public class CustomerSearchReadPlatformServiceImpl implements CustomerSearchRead
final StringBuilder sql = new StringBuilder(BASE_SELECT).append(" WHERE 1=1 ");
final List<Object> params = new ArrayList<>();
if (hasClientId) {
sql.append(" AND c.id = ? ");
params.add(clientIdValue);
}
if (hasName) {
sql.append(" AND LOWER(c.display_name) LIKE LOWER(?) ");
params.add("%" + name.trim() + "%");
}
if (hasMobile) {
// Partial, so the last digits of a number find it as well as the whole of it.
sql.append(" AND c.mobile_no LIKE ? ");
params.add("%" + mobileNo.trim() + "%");
}
if (hasAccountNumber) {
sql.append(" AND acct.account_no = ? ");
params.add(accountNumber.trim());
@@ -146,15 +131,6 @@ public class CustomerSearchReadPlatformServiceImpl implements CustomerSearchRead
return this.jdbcTemplate.query(sql.toString(), new CustomerSearchResultMapper(), params.toArray());
}
/** A client id is a whole number; anything else is the caller's mistake to be told about, not a server error. */
private static Long parseClientId(final String clientId) {
try {
return Long.valueOf(clientId.trim());
} catch (final NumberFormatException e) {
throw new IllegalArgumentException("Client ID must be a number.");
}
}
private static final class CustomerSearchResultMapper implements RowMapper<CustomerSearchResultData> {
@Override
public CustomerSearchResultData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
@@ -19,13 +19,9 @@
package org.apache.fineract.portfolio.dds.accounting;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
@@ -87,16 +83,35 @@ import org.springframework.stereotype.Component;
@RequiredArgsConstructor
public class DdsLedger {
/** Cash an agent has collected but not yet handed to a branch. Set by 0493. */
public static final String AGENT_CASH = "dds-agent-cash-account";
/** What an agent's commission costs the bank. Set by 0491. */
public static final String COMMISSION_EXPENSE = "dds-agent-commission-expense-account";
/** The savings product DDS opens customer accounts under. Set by 0492. */
public static final String DEPOSIT_PRODUCT = "dds-deposit-product";
/** What DDS customers are owed on accounts that are DDS's own rather than savings accounts. Set by 0516. */
public static final String DEPOSIT_LIABILITY = "dds-deposit-liability-account";
/** What interest on DDS's own accounts costs the bank. Set by 0518. */
public static final String INTEREST_EXPENSE = "dds-interest-expense-account";
private static final String ORGANISATION_CURRENCY_SQL = "select code from m_organisation_currency order by id limit 1";
private static final String PRODUCT_CURRENCY_SQL = "select currency_code from m_savings_product where id = ?";
private static final String CONFIGURED_ACCOUNT_SQL = "select value from c_configuration where name = ? and enabled = true";
/**
* The accounts money can be parked in for a customer with no account here. Every bank names its
* Sundry account differently, so none is assumed: every open posting liability account is offered
* and the clerk searches it by code or name and picks their bank's own. Liability, because the bank
* now owes the money - an income or expense account would book it as the bank's own. Posting,
* because a header cannot take an entry. And never an account whose balance is meant to equal the
* sum of customer or agent accounts - a DDS product's customer deposits account, a savings or
* deposit product's control account, an agent's own cash account - since parked money in any of
* sum of customer or agent accounts - DDS Customer Deposits, a savings or deposit product's control
* account, a current-account control, an agent's own cash account - since parked money in any of
* those would leave it no longer adding up.
*/
private static final String SUNDRY_ACCOUNTS_SQL = "select g.id as \"id\", g.gl_code as \"glCode\", g.name as \"name\" "
@@ -105,7 +120,8 @@ public class DdsLedger {
+ "and g.id not in (select m.gl_account_id from acc_product_mapping m where m.product_type = 2 "
+ "and m.financial_account_type = 2 and m.gl_account_id is not null) "
+ "and g.id not in (select a.gl_account_id from m_dds_agent a where a.gl_account_id is not null) "
+ "and g.id not in (select t.deposit_liability_gl_id from m_dds_account_type t where t.deposit_liability_gl_id is not null) "
+ "and g.id not in (select c.value from c_configuration c where c.name in ('dds-deposit-liability-account', "
+ "'dds-current-account-control-account', 'dds-agent-cash-account') and c.value is not null) "
+ "order by g.gl_code";
/**
@@ -148,7 +164,7 @@ public class DdsLedger {
* Deposits, so a transfer between them writes no leg, yet the slip still has to name both.
*/
public void voucherSideOnDeposits(final String entryType, final Party party, final BigDecimal amount, final String description) {
DdsVoucherScope.addLine(new DdsVoucherScope.Line(entryType, depositLiabilityOf(party).getId(),
DdsVoucherScope.addLine(new DdsVoucherScope.Line(entryType, configured(DEPOSIT_LIABILITY).getId(),
party.getPartyCode() + " · " + party.getPartyName(), party.getAccountNo(), amount, description));
}
@@ -167,6 +183,17 @@ public class DdsLedger {
return deposit;
}
/**
* Moves cash from an agent's hands into a branch. No customer account is involved - the customers
* were credited when their money was collected; this is the bank catching up with the cash.
*/
public void settle(final Office office, final LocalDate date, final BigDecimal amount, final String description,
final GLAccount agentCashAccount) {
final Long productId = depositProductId();
postPair(office, currencyOf(productId), date, amount, productAccount(productId, FUND_SOURCE), agentCashOr(agentCashAccount),
description, null, null);
}
/**
* Hands the customers of one application their money from the cashier's DDS receipts, in one entry.
*
@@ -180,13 +207,11 @@ public class DdsLedger {
* DR the account the receipts credited the agent's account clears
* CR DDS Customer Deposits what the bank now owes the customers
* </pre>
*
* One pair per customer-deposits account rather than one in all: each DDS product names its own
* (see depositLiabilityOf), so customers on two products are owed on two accounts.
*/
public void applyAgentReceipts(final Office office, final LocalDate date, final BigDecimal amount, final GLAccount receiptAccount,
final GLAccount depositAccount, final String transactionId, final String description) {
writePair(office, organisationCurrency(), transactionId, date, amount, receiptAccount, depositAccount, description, null, null);
final String transactionId, final String description) {
writePair(office, organisationCurrency(), transactionId, date, amount, receiptAccount, configured(DEPOSIT_LIABILITY), description,
null, null);
}
/**
@@ -217,7 +242,7 @@ public class DdsLedger {
collect(party.getSavingsAccountId(), date, amount, description, agentCashAccount);
return;
}
ddsPair(party, date, amount, agentCashOr(agentCashAccount), depositLiabilityOf(party), description);
ddsPair(party, date, amount, agentCashOr(agentCashAccount), configured(DEPOSIT_LIABILITY), description);
}
/** Interest onto the party's account: Dr interest expense, Cr what the customer is owed. */
@@ -226,7 +251,7 @@ public class DdsLedger {
postInterest(party.getSavingsAccountId(), date, amount, description);
return;
}
ddsPair(party, date, amount, interestExpenseOf(party), depositLiabilityOf(party), description);
ddsPair(party, date, amount, configured(INTEREST_EXPENSE), configured(DEPOSIT_LIABILITY), description);
}
/** Interest taken back on an early closure - the exact opposite of {@link #postInterest(Party, LocalDate, BigDecimal, String)}. */
@@ -235,7 +260,7 @@ public class DdsLedger {
reverseInterest(party.getSavingsAccountId(), date, amount, description);
return;
}
ddsPair(party, date, amount, depositLiabilityOf(party), interestExpenseOf(party), description);
ddsPair(party, date, amount, configured(DEPOSIT_LIABILITY), configured(INTEREST_EXPENSE), description);
}
/**
@@ -250,7 +275,7 @@ public class DdsLedger {
}
final SavingsAccount to = load(toSavingsAccountId);
final SavingsAccountTransaction deposit = depositWithoutAccounting(to, date, amount);
postPair(to, deposit, date, amount, depositLiabilityOf(party), controlAccountOf(to), description);
postPair(to, deposit, date, amount, configured(DEPOSIT_LIABILITY), controlAccountOf(to), description);
}
/**
@@ -270,23 +295,24 @@ public class DdsLedger {
final String description) {
final GLAccount owed;
final Office office;
Long withdrawalId = null;
Long savingsAccountId = null;
if (party.getSavingsAccountId() != null) {
final SavingsAccount account = load(party.getSavingsAccountId());
withdrawalId = withdraw(account, date, amount).getId();
savingsAccountId = account.getId();
final boolean wasBlocked = unlockDebitsIfBlocked(account);
final SavingsTransactionBooleanValues flags = new SavingsTransactionBooleanValues(false, true, false, false, false, true);
this.savingsAccountDomainService.handleWithdrawal(account, DateUtils.DEFAULT_DATE_FORMATTER, date, amount, null, flags,
this.savingsAccountAssembler.getPivotConfigStatus());
relockDebits(account, wasBlocked);
owed = controlAccountOf(account);
office = account.office();
} else {
owed = depositLiabilityOf(party);
office = officeOf(party);
owed = configured(DEPOSIT_LIABILITY);
office = party.getOffice() != null ? party.getOffice() : party.getAgent().getOffice();
}
creditCurrentAccount(currentAccountId, date, amount, description);
final String transactionId = withdrawalId != null ? "S" + withdrawalId : partyLegId(party);
final String transactionId = "DDS-" + party.getPartyCode() + "-" + UUID.randomUUID().toString().substring(0, 8);
writePair(office, organisationCurrency(), transactionId, date, amount, owed, currentAccountControl(currentAccountId), description,
withdrawalId, savingsAccountId);
null, null);
}
private static final String CURRENT_ACCOUNT_SQL = "select status_enum, account_balance_derived, office_id from m_current_account where id = ?";
@@ -303,6 +329,9 @@ public class DdsLedger {
+ " join acc_product_mapping apm on apm.product_id = ca.product_id "
+ " where ca.id = ? and apm.product_type = 2 and apm.financial_account_type = 2 and apm.payment_type is null";
private static final String CURRENT_CONTROL_OVERRIDE_SQL = "select value from c_configuration "
+ " where name = 'dds-current-account-control-account' and enabled = true";
/** A deposit row and the balances on the current account, refused unless it is active. */
private void creditCurrentAccount(final Long currentAccountId, final LocalDate date, final BigDecimal amount, final String description) {
final List<java.util.Map<String, Object>> rows = this.jdbcTemplate.queryForList(CURRENT_ACCOUNT_SQL, currentAccountId); // NOSONAR
@@ -319,16 +348,23 @@ public class DdsLedger {
this.jdbcTemplate.update(CURRENT_BALANCE_SQL, amount, amount, currentAccountId); // NOSONAR
}
/** The current account's control account, from the current product's own accounting. */
/**
* The current account's control account: its product's mapping when that is the only one, and the
* setting when the product shares an id - and so its mapping rows - with a savings product.
*/
private GLAccount currentAccountControl(final Long currentAccountId) {
final List<Long> ids = this.jdbcTemplate.queryForList(CURRENT_CONTROL_SQL, Long.class, currentAccountId); // NOSONAR
if (ids.size() == 1) {
return this.glAccountRepository.findById(ids.get(0))
.orElseThrow(() -> new DdsLedgerNotConfiguredException("control account for current account " + currentAccountId));
}
final List<Long> override = this.jdbcTemplate.queryForList(CURRENT_CONTROL_OVERRIDE_SQL, Long.class); // NOSONAR
if (!override.isEmpty() && override.get(0) != null) {
return this.glAccountRepository.findById(override.get(0))
.orElseThrow(() -> new DdsLedgerNotConfiguredException("dds-current-account-control-account"));
}
throw new DdsLedgerNotConfiguredException(ids.isEmpty() ? "control account for current account " + currentAccountId
: "a single control account for current account " + currentAccountId
+ " (its current product's GL mapping is shared with a savings product of the same id)");
: "dds-current-account-control-account (the current product's GL mapping is shared with a savings product of the same id)");
}
/** The party's balance parked in the Sundry account chosen for it - see sundryAccount. */
@@ -338,7 +374,7 @@ public class DdsLedger {
payOutToPayable(party.getSavingsAccountId(), date, amount, description, parkIn);
return;
}
ddsPair(party, date, amount, depositLiabilityOf(party), parkIn, description);
ddsPair(party, date, amount, configured(DEPOSIT_LIABILITY), parkIn, description);
}
/** The accounts a customer's balance may be parked in, by GL code. None is chosen for the clerk. */
@@ -381,191 +417,44 @@ public class DdsLedger {
transfer(from.getSavingsAccountId(), to.getSavingsAccountId(), date, amount, description);
} else if (fromSavings) {
final SavingsAccount account = load(from.getSavingsAccountId());
final SavingsAccountTransaction withdrawal = withdraw(account, date, amount);
final boolean wasBlocked = unlockDebitsIfBlocked(account);
final SavingsTransactionBooleanValues flags = new SavingsTransactionBooleanValues(false, true, false, false, false, true);
this.savingsAccountDomainService.handleWithdrawal(account, DateUtils.DEFAULT_DATE_FORMATTER, date, amount, null, flags,
this.savingsAccountAssembler.getPivotConfigStatus());
relockDebits(account, wasBlocked);
postPair(account.office(), account.getCurrency().getCode(), date, amount, controlAccountOf(account),
depositLiabilityOf(from), description, withdrawal.getId(), account.getId());
configured(DEPOSIT_LIABILITY), description, null, account.getId());
} else if (toSavings) {
final SavingsAccount account = load(to.getSavingsAccountId());
final SavingsAccountTransaction deposit = depositWithoutAccounting(account, date, amount);
postPair(account, deposit, date, amount, depositLiabilityOf(to), controlAccountOf(account), description);
postPair(account, deposit, date, amount, configured(DEPOSIT_LIABILITY), controlAccountOf(account), description);
} else {
final GLAccount fromDeposits = depositLiabilityOf(from);
final GLAccount toDeposits = depositLiabilityOf(to);
if (fromDeposits.getId().equals(toDeposits.getId())) {
// Both owed on the same account: nothing to book, but the slip still names who paid whom -
// and the two passbook lines claim no journal, since none was written for them.
DdsVoucherScope.forgetLastLedgerTransaction();
voucherSideOnDeposits("DR", from, amount, description);
voucherSideOnDeposits("CR", to, amount, description);
} else {
// Two products with customer-deposit accounts of their own: the debt moves from one to the other.
ddsPair(from, date, amount, fromDeposits, toDeposits, description);
}
// Both on DDS Customer Deposits: nothing to book, but the slip still names who paid whom.
voucherSideOnDeposits("DR", from, amount, description);
voucherSideOnDeposits("CR", to, amount, description);
}
}
/** One pair for a party with no savings account: at the party's own branch, in the bank's currency. */
private void ddsPair(final Party party, final LocalDate date, final BigDecimal amount, final GLAccount debit, final GLAccount credit,
final String description) {
writePair(officeOf(party), organisationCurrency(), partyLegId(party), date, amount, debit, credit, description, null, null);
final Office office = party.getOffice() != null ? party.getOffice() : party.getAgent().getOffice();
final String transactionId = "DDS-" + party.getPartyCode() + "-" + UUID.randomUUID().toString().substring(0, 8);
writePair(office, organisationCurrency(), transactionId, date, amount, debit, credit, description, null, null);
}
/**
* The account playing the agent's part on an entry - the agent's own. An agent cannot be saved
* without one, so a missing one here is an agent set up before that was required.
* Which account plays the agent's part on an entry: the agent's own, or the scheme's shared one
* when they have none.
*
* Only the account differs. Both sides of the entry, their direction and the amount are the same
* either way - an agent with their own account is not accounted for differently, only separately.
* That is what makes the shared account a safe fallback rather than a second behaviour: an agent
* set up before this existed posts exactly as they always did, and a branch can move agents onto
* their own accounts one at a time.
*/
private GLAccount agentCashOr(final GLAccount agentCashAccount) {
if (agentCashAccount == null) {
throw new DdsLedgerNotConfiguredException("the agent's GL account (Organization > Agent Master)");
}
return agentCashAccount;
}
// ---------------------------------------------------------------------------------------------
// A party's product's own ledger accounts.
//
// Each DDS product names the accounts it posts through on its Accounting tab (0620), as a savings
// product does, and those are the only accounts DDS posts through - there is no bank-wide account
// behind them. A product that has not named one stops the posting with a message saying which
// account to set on which product, rather than the money landing somewhere nobody chose.
// ---------------------------------------------------------------------------------------------
/** Columns of m_dds_account_type a product's account may be read from - never anything a caller typed. */
private static final Set<String> PRODUCT_GL_COLUMNS = Set.of("deposit_liability_gl_id", "interest_expense_gl_id",
"commission_expense_gl_id");
/** What the bank owes this party - their product's customer-deposits account. */
public GLAccount depositLiabilityOf(final Party party) {
return productAccount(party, "deposit_liability_gl_id", "DDS Customer Deposits");
}
/** What this party's interest costs the bank - their product's interest-expense account. */
public GLAccount interestExpenseOf(final Party party) {
return productAccount(party, "interest_expense_gl_id", "Interest Expense");
}
private GLAccount productAccount(final Party party, final String column, final String label) {
if (!PRODUCT_GL_COLUMNS.contains(column)) {
throw new IllegalArgumentException(column);
}
final String productCode = party == null || party.getSubType() == null ? null : party.getSubType().trim().toUpperCase();
if (productCode == null) {
throw new DdsLedgerNotConfiguredException("a DDS product for party " + (party == null ? "?" : party.getPartyCode()));
}
// By the id the account is tied to; by its code only for an account saved before ids were kept.
final List<Long> ids = party.getAccountTypeId() != null
? this.jdbcTemplate.queryForList("select " + column + " from m_dds_account_type where id = ?", Long.class, // NOSONAR
party.getAccountTypeId())
: this.jdbcTemplate.queryForList("select " + column + " from m_dds_account_type where account_type_code = ?", // NOSONAR
Long.class, productCode);
if (ids.isEmpty()) {
throw new DdsLedgerNotConfiguredException("DDS product " + productCode + " (party " + party.getPartyCode() + ")");
}
return accountNamed(ids.get(0), productCode, label);
}
private GLAccount accountNamed(final Long glAccountId, final String productCode, final String label) {
final Optional<GLAccount> account = glAccountId == null ? Optional.empty() : this.glAccountRepository.findById(glAccountId);
return account.orElseThrow(() -> new DdsLedgerNotConfiguredException(
"the " + label + " account on DDS product " + productCode + " (Products > DDS Products > Edit > Accounting)"));
}
// ---------------------------------------------------------------------------------------------
// An agent's commission, by product.
//
// Commission is paid to an agent in one sum for a round that may collect for several products, and
// each product books its share on its own commission account. The share is each product's part of
// the collections the commission was earned on: those a commission run has counted and no payment
// has yet covered. Paying marks them, so the next payment is shared out on the next collections.
// ---------------------------------------------------------------------------------------------
private static final String COMMISSION_BASIS_SQL = "select upper(trim(p.sub_type)) as \"product\", t.commission_expense_gl_id as \"glId\", "
+ "sum(pt.amount) as \"volume\" from m_dds_party_transaction pt join m_dds_party p on p.id = pt.party_id "
+ "left join m_dds_account_type t on t.account_type_code = upper(trim(p.sub_type)) "
+ "where pt.agent_id = ? and pt.transaction_type = 'PARTY_TRAN' and pt.direction = 'CREDIT' "
+ "and pt.commission_batch_run_id is not null and pt.commission_batch_run_id < ? %s group by upper(trim(p.sub_type)), t.commission_expense_gl_id "
+ "order by upper(trim(p.sub_type))";
private static final String UNPAID_COMMISSION_FILTER = "and pt.commission_paid_batch_run_id is null";
private static final String MARK_COMMISSION_PAID_SQL = "update m_dds_party_transaction set commission_paid_batch_run_id = ? "
+ "where agent_id = ? and transaction_type = 'PARTY_TRAN' and direction = 'CREDIT' and commission_batch_run_id is not null "
+ "and commission_batch_run_id < ? and commission_paid_batch_run_id is null";
/**
* An agent's commission split across the commission accounts of the products they collected for,
* in proportion to what they collected for each. The last share takes the rounding, so the shares
* always add up to the amount paid.
*
* Only collections counted by commission runs before the paying run are its basis: a run counted
* after this payment was generated earned commission this payment does not include. Collections
* counted before payments were tracked by run have no payment marked against them; if nothing
* unpaid is left, the agent's counted collections as a whole set the proportions instead.
*/
public Map<GLAccount, BigDecimal> commissionSplit(final Long agentId, final BigDecimal amount, final Long payingRunId) {
List<Map<String, Object>> basis = this.jdbcTemplate.queryForList(String.format(COMMISSION_BASIS_SQL, UNPAID_COMMISSION_FILTER), // NOSONAR
agentId, payingRunId);
if (basis.isEmpty()) {
basis = this.jdbcTemplate.queryForList(String.format(COMMISSION_BASIS_SQL, ""), agentId, payingRunId); // NOSONAR
}
return shareOut(basis, amount);
}
/** The collections a commission run would count if it were posted now: not counted by any run yet, up to a date. */
private static final String COMMISSION_PENDING_SQL = "select upper(trim(p.sub_type)) as \"product\", t.commission_expense_gl_id as \"glId\", "
+ "sum(pt.amount) as \"volume\" from m_dds_party_transaction pt join m_dds_party p on p.id = pt.party_id "
+ "left join m_dds_account_type t on t.account_type_code = upper(trim(p.sub_type)) "
+ "where pt.agent_id = ? and pt.transaction_type = 'PARTY_TRAN' and pt.direction = 'CREDIT' "
+ "and pt.commission_batch_run_id is null and pt.transaction_date <= ? group by upper(trim(p.sub_type)), "
+ "t.commission_expense_gl_id order by upper(trim(p.sub_type))";
/**
* Where an agent's commission will be booked once it is posted - for a calculation run, which books
* nothing itself but whose approval should say which commission accounts the Posting run will debit.
* The split is over the collections the calculation counts (none counted yet, up to its end date).
*/
public Map<GLAccount, BigDecimal> projectedCommissionSplit(final Long agentId, final BigDecimal amount, final LocalDate through) {
return shareOut(this.jdbcTemplate.queryForList(COMMISSION_PENDING_SQL, agentId, java.sql.Date.valueOf(through)), amount); // NOSONAR
}
/** Shares an amount over the commission accounts of the products collected for, by volume; the last takes the rounding. */
private Map<GLAccount, BigDecimal> shareOut(final List<Map<String, Object>> basis, final BigDecimal amount) {
if (basis.isEmpty()) {
throw new DdsLedgerNotConfiguredException("a collection by this agent to tell which DDS product's Agent Commission Expense "
+ "account the commission belongs to");
}
final Map<Long, GLAccount> accounts = new LinkedHashMap<>();
final Map<Long, BigDecimal> volumes = new LinkedHashMap<>();
BigDecimal total = BigDecimal.ZERO;
for (final Map<String, Object> row : basis) {
final String product = (String) row.get("product");
final Long glId = row.get("glId") == null ? null : ((Number) row.get("glId")).longValue();
final GLAccount account = accountNamed(glId, product, "Agent Commission Expense");
final BigDecimal volume = (BigDecimal) row.get("volume");
accounts.putIfAbsent(account.getId(), account);
volumes.merge(account.getId(), volume, BigDecimal::add);
total = total.add(volume);
}
final Map<GLAccount, BigDecimal> shares = new LinkedHashMap<>();
BigDecimal allotted = BigDecimal.ZERO;
final List<Long> ids = List.copyOf(volumes.keySet());
for (int i = 0; i < ids.size(); i++) {
final Long id = ids.get(i);
final BigDecimal share = i == ids.size() - 1 || total.signum() == 0 ? amount.subtract(allotted)
: amount.multiply(volumes.get(id)).divide(total, 2, RoundingMode.HALF_UP);
allotted = allotted.add(share);
if (share.signum() != 0) {
shares.put(accounts.get(id), share);
}
}
return shares;
}
/** Marks the collections a commission payment was shared out on, so the next payment starts after them. */
public void markCommissionPaid(final Long agentId, final Long batchRunId) {
this.jdbcTemplate.update(MARK_COMMISSION_PAID_SQL, batchRunId, agentId, batchRunId); // NOSONAR
return agentCashAccount != null ? agentCashAccount : configured(AGENT_CASH);
}
/**
@@ -601,9 +490,15 @@ public class DdsLedger {
public void reverseInterest(final Long savingsAccountId, final LocalDate date, final BigDecimal amount, final String description) {
final SavingsAccount account = load(savingsAccountId);
final SavingsAccountTransaction withdrawal = withdraw(account, date, amount);
// DDS's own movement, so the debit block comes off for it and goes straight back on.
final boolean wasBlocked = unlockDebitsIfBlocked(account);
final SavingsTransactionBooleanValues flags = new SavingsTransactionBooleanValues(false, true, false, false, false, true);
this.savingsAccountDomainService.handleWithdrawal(account, DateUtils.DEFAULT_DATE_FORMATTER, date, amount, null, flags,
this.savingsAccountAssembler.getPivotConfigStatus());
relockDebits(account, wasBlocked);
postPair(account.office(), account.getCurrency().getCode(), date, amount, controlAccountOf(account),
productAccount(account.productId(), INTEREST_ON_SAVINGS), description, withdrawal.getId(), account.getId());
productAccount(account.productId(), INTEREST_ON_SAVINGS), description, null, account.getId());
}
/**
@@ -620,15 +515,15 @@ public class DdsLedger {
// The scheme account is blocked for debit so nobody can take the money out at a counter. This
// movement is DDS's own, so the block comes off for it and goes straight back on.
final SavingsAccountTransaction withdrawal = withdraw(from, date, amount);
final boolean wasBlocked = unlockDebitsIfBlocked(from);
final SavingsTransactionBooleanValues withdrawalFlags = new SavingsTransactionBooleanValues(false, true, false, false, false, true);
this.savingsAccountDomainService.handleWithdrawal(from, DateUtils.DEFAULT_DATE_FORMATTER, date, amount, null, withdrawalFlags,
this.savingsAccountAssembler.getPivotConfigStatus());
relockDebits(from, wasBlocked);
final SavingsAccountTransaction deposit = depositWithoutAccounting(to, date, amount);
// One leg on each account, each carrying its own account transaction, so both sides of the
// movement can be traced from the journal - not only the deposit.
writeLeg(from.office(), from.getCurrency().getCode(), "S" + withdrawal.getId(), date, amount, controlAccountOf(from),
JournalEntryType.DEBIT, description, withdrawal.getId(), from.getId());
writeLeg(from.office(), from.getCurrency().getCode(), "S" + withdrawal.getId(), date, amount, controlAccountOf(to),
JournalEntryType.CREDIT, description, deposit.getId(), to.getId());
postPair(to, deposit, date, amount, controlAccountOf(from), controlAccountOf(to), description);
}
/**
@@ -661,9 +556,15 @@ public class DdsLedger {
final GLAccount parkIn) {
final SavingsAccount account = load(savingsAccountId);
final SavingsAccountTransaction withdrawal = withdraw(account, date, amount);
postPair(account.office(), account.getCurrency().getCode(), date, amount, controlAccountOf(account), parkIn, description,
withdrawal.getId(), account.getId());
// DDS's own movement, so the debit block comes off for it and goes straight back on.
final boolean wasBlocked = unlockDebitsIfBlocked(account);
final SavingsTransactionBooleanValues flags = new SavingsTransactionBooleanValues(false, true, false, false, false, true);
this.savingsAccountDomainService.handleWithdrawal(account, DateUtils.DEFAULT_DATE_FORMATTER, date, amount, null, flags,
this.savingsAccountAssembler.getPivotConfigStatus());
relockDebits(account, wasBlocked);
postPair(account.office(), account.getCurrency().getCode(), date, amount, controlAccountOf(account), parkIn,
description, null, account.getId());
}
/**
@@ -691,18 +592,12 @@ public class DdsLedger {
return deposit;
}
/**
* Credits an agent's own savings account with commission the bank owes them - one deposit to the
* agent, and one entry per product on that product's commission account (see commissionSplit).
*/
/** Credits an agent's own savings account with commission the bank owes them. */
public SavingsAccountTransaction payCommission(final Long savingsAccountId, final LocalDate date, final BigDecimal amount,
final String description, final Map<GLAccount, BigDecimal> shares) {
final String description) {
final SavingsAccount account = load(savingsAccountId);
final SavingsAccountTransaction deposit = depositWithoutAccounting(account, date, amount);
final GLAccount control = controlAccountOf(account);
for (final Map.Entry<GLAccount, BigDecimal> share : shares.entrySet()) {
postPair(account, deposit, date, share.getValue(), share.getKey(), control, description);
}
postPair(account, deposit, date, amount, configured(COMMISSION_EXPENSE), controlAccountOf(account), description);
return deposit;
}
@@ -772,60 +667,47 @@ public class DdsLedger {
private void postPair(final Office office, final String currencyCode, final LocalDate date, final BigDecimal amount,
final GLAccount debit, final GLAccount credit, final String description, final Long savingsTransactionId,
final Long savingsAccountId) {
final String transactionId = savingsTransactionId == null ? "DDS-" + UUID.randomUUID() : "S" + savingsTransactionId;
final String transactionId = savingsTransactionId == null ? "DDS" + date + amount : "S" + savingsTransactionId;
writePair(office, currencyCode, transactionId, date, amount, debit, credit, description, savingsTransactionId, savingsAccountId);
}
private void writePair(final Office office, final String currencyCode, final String transactionId, final LocalDate date,
final BigDecimal amount, final GLAccount debit, final GLAccount credit, final String description,
final Long savingsTransactionId, final Long savingsAccountId) {
writeLeg(office, currencyCode, transactionId, date, amount, debit, JournalEntryType.DEBIT, description, savingsTransactionId,
savingsAccountId);
writeLeg(office, currencyCode, transactionId, date, amount, credit, JournalEntryType.CREDIT, description, savingsTransactionId,
savingsAccountId);
}
private void writeLeg(final Office office, final String currencyCode, final String transactionId, final LocalDate date,
final BigDecimal amount, final GLAccount account, final JournalEntryType type, final String description,
final Long savingsTransactionId, final Long savingsAccountId) {
// Noted for the voucher of the operation this leg belongs to - see DdsVoucherService - and as the
// journal the next passbook line of the operation is linked to (m_dds_party_transaction.journal_transaction_id).
// Noted for the voucher of the operation this leg belongs to - see DdsVoucherService.
DdsVoucherScope.recordLedgerTransaction(transactionId);
this.journalEntryRepository.saveAndFlush(JournalEntry.createNew(office, null, account, currencyCode, transactionId, false, date, type,
amount, description, PortfolioProductType.SAVING.getValue(), savingsAccountId, null, null, savingsTransactionId, null,
null));
for (final JournalEntryType type : List.of(JournalEntryType.DEBIT, JournalEntryType.CREDIT)) {
this.journalEntryRepository.saveAndFlush(JournalEntry.createNew(office, null,
JournalEntryType.DEBIT.equals(type) ? debit : credit, currencyCode, transactionId, false, date, type, amount,
description, PortfolioProductType.SAVING.getValue(), savingsAccountId, null, null, savingsTransactionId, null, null));
}
}
/**
* A journal id for a leg on DDS's own books - the party's code to find it by and a full UUID to keep it
* unique: DDS-PT0003-1b4e... Never built from the date and amount, which two parties can share.
*/
private static String partyLegId(final Party party) {
return "DDS-" + party.getPartyCode() + "-" + UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
/** The branch a party's legs are booked at: the party's own, or its agent's for a party opened without one. */
public Office officeOf(final Party party) {
return party.getOffice() != null ? party.getOffice() : party.getAgent().getOffice();
}
/**
* Takes money off a scheme account that DDS keeps blocked for debits, as DDS's own movement - the block
* comes off for it and goes straight back on - and returns the withdrawal so its journal can name it.
*/
private SavingsAccountTransaction withdraw(final SavingsAccount account, final LocalDate date, final BigDecimal amount) {
final boolean wasBlocked = unlockDebitsIfBlocked(account);
final SavingsTransactionBooleanValues flags = new SavingsTransactionBooleanValues(false, true, false, false, false, true);
final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(account,
DateUtils.DEFAULT_DATE_FORMATTER, date, amount, null, flags, this.savingsAccountAssembler.getPivotConfigStatus());
relockDebits(account, wasBlocked);
return withdrawal;
/** A ledger account named by a setting, so a bank can point it at one of their own. */
private GLAccount configured(final String settingName) {
final List<Long> ids = this.jdbcTemplate.queryForList(CONFIGURED_ACCOUNT_SQL, Long.class, settingName); // NOSONAR
if (ids.isEmpty() || ids.get(0) == null) {
throw new DdsLedgerNotConfiguredException(settingName);
}
return this.glAccountRepository.findById(ids.get(0)).orElseThrow(() -> new DdsLedgerNotConfiguredException(settingName));
}
private GLAccount controlAccountOf(final SavingsAccount account) {
return productAccount(account.productId(), CONTROL_ACCOUNT);
}
/**
* The savings product DDS opens customer accounts under. A settlement touches no single customer,
* so its ledger accounts and currency come from the product rather than from an account.
*/
private Long depositProductId() {
final List<Long> ids = this.jdbcTemplate.queryForList(CONFIGURED_ACCOUNT_SQL, Long.class, DEPOSIT_PRODUCT); // NOSONAR
if (ids.isEmpty() || ids.get(0) == null) {
throw new DdsLedgerNotConfiguredException(DEPOSIT_PRODUCT);
}
return ids.get(0);
}
/** The bank's own currency, for an entry that belongs to no savings product. */
private String organisationCurrency() {
final List<String> codes = this.jdbcTemplate.queryForList(ORGANISATION_CURRENCY_SQL, String.class); // NOSONAR
@@ -835,6 +717,14 @@ public class DdsLedger {
return codes.get(0);
}
/** Read off the product rather than assumed, so a bank on another currency is not silently wrong. */
private String currencyOf(final Long productId) {
final List<String> codes = this.jdbcTemplate.queryForList(PRODUCT_CURRENCY_SQL, String.class, productId); // NOSONAR
if (codes.isEmpty()) {
throw new DdsLedgerNotConfiguredException("currency for savings product " + productId);
}
return codes.get(0);
}
/** An account off the savings product's own mapping, so this stays right per product. */
private GLAccount productAccount(final Long productId, final int financialAccountType) {
@@ -22,7 +22,6 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -336,9 +335,6 @@ public class DdsVoucherService {
LAST_ISSUED.set(voucherNo);
stamp("update m_dds_party_transaction set voucher_no = ? where id in (%s)", voucherNo, scope.getPartyTransactionIds());
// The journal legs carry the voucher too, in ref_num, so the slip, the passbook lines and the
// journal can each be found from the others - as a savings voucher's legs can.
stampLegs(voucherNo, scope.getLedgerTransactionIds());
stamp("update m_savings_account_transaction set voucher_no = ? where voucher_no is null and id in (%s)", voucherNo,
savingsTransactions);
}
@@ -360,7 +356,7 @@ public class DdsVoucherService {
return new ArrayList<>();
}
final Set<Long> depositGlIds = depositLiabilityGlIds();
final Long depositGlId = depositLiabilityGlId();
final Map<String, Map<String, Object>> parties = new LinkedHashMap<>();
for (final Map<String, Object> leg : this.jdbcTemplate.queryForList(String.format(LEGS_SQL, placeholders(transactionIds)), // NOSONAR
transactionIds.toArray())) {
@@ -387,7 +383,7 @@ public class DdsVoucherService {
}
final Matcher partyLeg = PARTY_LEG.matcher(String.valueOf(leg.get("transactionId")));
if (partyLeg.matches() && depositGlIds.contains(glId)) {
if (partyLeg.matches() && glId.equals(depositGlId)) {
final String partyCode = partyLeg.group(1);
final Map<String, Object> party = parties.computeIfAbsent(partyCode,
code -> this.jdbcTemplate.queryForList(PARTY_SQL, code).stream().findFirst().orElse(Map.of())); // NOSONAR
@@ -421,24 +417,11 @@ public class DdsVoucherService {
return lines;
}
/**
* The GLs that carry what DDS customers are owed - a leg on one is the customer's own. Each DDS
* product names its own on its Accounting tab (0620).
*/
private Set<Long> depositLiabilityGlIds() {
return new HashSet<>(this.jdbcTemplate.queryForList( // NOSONAR
"select deposit_liability_gl_id from m_dds_account_type where deposit_liability_gl_id is not null", Long.class));
}
private void stampLegs(final String voucherNo, final Collection<String> transactionIds) {
if (transactionIds.isEmpty()) {
return;
}
final List<Object> args = new ArrayList<>();
args.add(voucherNo);
args.addAll(transactionIds);
this.jdbcTemplate.update("update acc_gl_journal_entry set ref_num = ? where ref_num is null and transaction_id in (" // NOSONAR
+ placeholders(transactionIds) + ")", args.toArray());
/** The GL that carries what DDS customers are owed - a leg on it is the customer's own. */
private Long depositLiabilityGlId() {
final List<Long> ids = this.jdbcTemplate.queryForList( // NOSONAR
"select value from c_configuration where name = 'dds-deposit-liability-account' and enabled = true", Long.class);
return ids.isEmpty() ? null : ids.get(0);
}
private void stamp(final String sql, final String voucherNo, final Collection<Long> ids) {
@@ -27,7 +27,11 @@ public interface DdsAccountTypeRepository extends JpaRepository<DdsAccountType,
Optional<DdsAccountType> findByAccountTypeCode(String accountTypeCode);
/** Schemes that post interest on their own, at the end of each posting period. */
@Query("select t from DdsAccountType t where t.active = true and t.autoInterestPost = true order by t.accountTypeCode")
/**
* Schemes that post interest on their own. A posting day is required to act on one, so a scheme
* left without a day is not picked up - it would otherwise mean "post monthly, some day".
*/
@Query("select t from DdsAccountType t where t.active = true and t.autoInterestPost = true "
+ "and t.autoInterestPostDay is not null order by t.accountTypeCode")
List<DdsAccountType> findDueForAutoInterestPost();
}
@@ -39,15 +39,9 @@ import org.springframework.stereotype.Component;
@Component
public final class DdsAccountTypeCommandFromApiJsonDeserializer {
/*
* "autoInterestPostDay" is still accepted, and ignored, so a change held for approval before the
* posting day was retired - or an older screen still sending it - is not refused. Interest now
* posts at the end of each period, so there is no day to set.
*/
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList("accountTypeGroup", "accountTypeCode",
"accountTypeDescription", "autoInterestPost", "autoInterestPostWhen", "autoInterestPostDay", "maturityAutoClose", "active",
"prematureLockinMonths", "prematureInterestReduction", "prematureAgentCommissionPenalty", "interestRate",
"depositLiabilityGlId", "interestExpenseGlId", "commissionExpenseGlId",
"dateFormat", "locale"));
/** A rate, not an amount - nothing here may exceed the whole of what it is taken from. */
@@ -72,14 +66,6 @@ public final class DdsAccountTypeCommandFromApiJsonDeserializer {
v.reset().parameter("accountTypeDescription")
.value(this.fromApiJsonHelper.extractStringNamed("accountTypeDescription", element)).notBlank()
.notExceedingLengthOf(200);
// Mandatory, as a savings product's control and interest accounts are: every posting for the
// product's customers and agents goes through them, with nothing bank-wide behind them.
v.reset().parameter("depositLiabilityGlId").value(this.fromApiJsonHelper.extractLongNamed("depositLiabilityGlId", element))
.notNull().integerGreaterThanZero();
v.reset().parameter("interestExpenseGlId").value(this.fromApiJsonHelper.extractLongNamed("interestExpenseGlId", element))
.notNull().integerGreaterThanZero();
v.reset().parameter("commissionExpenseGlId").value(this.fromApiJsonHelper.extractLongNamed("commissionExpenseGlId", element))
.notNull().integerGreaterThanZero();
// Mandatory: every account opened on the scheme takes this rate.
v.reset().parameter("interestRate").value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRate", element))
.notNull().zeroOrPositiveAmount().notGreaterThanMax(HUNDRED);
@@ -113,14 +99,6 @@ public final class DdsAccountTypeCommandFromApiJsonDeserializer {
.notNull().zeroOrPositiveAmount().notGreaterThanMax(HUNDRED);
}
// Once named, a product's customer and interest accounts can be changed but not cleared.
for (final String required : List.of("depositLiabilityGlId", "interestExpenseGlId", "commissionExpenseGlId")) {
if (this.fromApiJsonHelper.parameterExists(required, element)) {
v.reset().parameter(required).value(this.fromApiJsonHelper.extractLongNamed(required, element)).notNull()
.integerGreaterThanZero();
}
}
validateOptionalFields(element, v);
throwExceptionIfValidationWarningsExist(dataValidationErrors);
@@ -143,6 +121,11 @@ public final class DdsAccountTypeCommandFromApiJsonDeserializer {
.value(this.fromApiJsonHelper.extractStringNamed("autoInterestPostWhen", element)).ignoreIfNull()
.notExceedingLengthOf(20);
}
if (this.fromApiJsonHelper.parameterExists("autoInterestPostDay", element)) {
v.reset().parameter("autoInterestPostDay")
.value(this.fromApiJsonHelper.extractIntegerSansLocaleNamed("autoInterestPostDay", element)).ignoreIfNull()
.inMinMaxRange(1, 31);
}
// Premature closure terms. Zero is allowed throughout - a scheme may close early from day
// one, or charge the customer or the agent nothing - so these are floored at zero, not at one.
@@ -43,24 +43,15 @@ public class DdsAccountTypeReadPlatformServiceImpl implements DdsAccountTypeRead
public String schema() {
return " t.id as id, t.account_type_group as accountTypeGroup, t.account_type_code as accountTypeCode, "
+ " t.account_type_description as accountTypeDescription, t.auto_interest_post as autoInterestPost, "
+ " t.auto_interest_post_when as autoInterestPostWhen, "
+ " t.auto_interest_post_when as autoInterestPostWhen, t.auto_interest_post_day as autoInterestPostDay, "
+ " t.maturity_auto_close as maturityAutoClose, "
+ " t.premature_lockin_months as prematureLockinMonths, "
+ " t.premature_interest_reduction as prematureInterestReduction, "
+ " t.premature_agent_commission_penalty as prematureAgentCommissionPenalty, "
+ " t.interest_rate as interestRate, "
+ " t.is_active as active, "
+ " t.created_date as createdDate, t.lastmodified_date as lastModifiedDate, "
+ " t.deposit_liability_gl_id as depositLiabilityGlId, dl.gl_code as depositLiabilityGlCode, "
+ " dl.name as depositLiabilityGlName, "
+ " t.interest_expense_gl_id as interestExpenseGlId, ie.gl_code as interestExpenseGlCode, "
+ " ie.name as interestExpenseGlName, "
+ " t.commission_expense_gl_id as commissionExpenseGlId, ce.gl_code as commissionExpenseGlCode, "
+ " ce.name as commissionExpenseGlName "
+ " from m_dds_account_type t "
+ " left join acc_gl_account dl on dl.id = t.deposit_liability_gl_id "
+ " left join acc_gl_account ie on ie.id = t.interest_expense_gl_id "
+ " left join acc_gl_account ce on ce.id = t.commission_expense_gl_id";
+ " t.created_date as createdDate, t.lastmodified_date as lastModifiedDate "
+ " from m_dds_account_type t";
}
@Override
@@ -72,21 +63,13 @@ public class DdsAccountTypeReadPlatformServiceImpl implements DdsAccountTypeRead
.accountTypeDescription(rs.getString("accountTypeDescription")) //
.autoInterestPost(rs.getBoolean("autoInterestPost")) //
.autoInterestPostWhen(rs.getString("autoInterestPostWhen")) //
.autoInterestPostDay(JdbcSupport.getInteger(rs, "autoInterestPostDay")) //
.maturityAutoClose(rs.getBoolean("maturityAutoClose")) //
.prematureLockinMonths(JdbcSupport.getInteger(rs, "prematureLockinMonths")) //
.prematureInterestReduction(rs.getBigDecimal("prematureInterestReduction")) //
.prematureAgentCommissionPenalty(rs.getBigDecimal("prematureAgentCommissionPenalty")) //
.interestRate(rs.getBigDecimal("interestRate")) //
.active(rs.getBoolean("active")) //
.depositLiabilityGlId(JdbcSupport.getLong(rs, "depositLiabilityGlId")) //
.depositLiabilityGlCode(rs.getString("depositLiabilityGlCode")) //
.depositLiabilityGlName(rs.getString("depositLiabilityGlName")) //
.interestExpenseGlId(JdbcSupport.getLong(rs, "interestExpenseGlId")) //
.interestExpenseGlCode(rs.getString("interestExpenseGlCode")) //
.interestExpenseGlName(rs.getString("interestExpenseGlName")) //
.commissionExpenseGlId(JdbcSupport.getLong(rs, "commissionExpenseGlId")) //
.commissionExpenseGlCode(rs.getString("commissionExpenseGlCode")) //
.commissionExpenseGlName(rs.getString("commissionExpenseGlName")) //
.createdDate(localDateTime(rs, "createdDate")) //
.lastModifiedDate(localDateTime(rs, "lastModifiedDate")) //
.build();
@@ -19,7 +19,6 @@
package org.apache.fineract.portfolio.dds.accounttype.service;
import jakarta.persistence.PersistenceException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
@@ -36,10 +35,7 @@ import org.apache.fineract.portfolio.dds.accounttype.exception.DdsAccountTypeNot
import org.apache.fineract.portfolio.dds.accounttype.serialization.DdsAccountTypeCommandFromApiJsonDeserializer;
import org.apache.fineract.portfolio.dds.party.domain.Party;
import org.apache.fineract.portfolio.dds.party.domain.PartyRepository;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.transaction.annotation.Transactional;
@@ -50,15 +46,6 @@ public class DdsAccountTypeWritePlatformServiceJpaRepositoryImpl implements DdsA
private final DdsAccountTypeCommandFromApiJsonDeserializer fromApiJsonDeserializer;
private final DdsAccountTypeRepository ddsAccountTypeRepository;
private final PartyRepository partyRepository;
private final JdbcTemplate jdbcTemplate;
private static final String GL_SQL = "select classification_enum, account_usage, disabled, gl_code, name from acc_gl_account where id = ?";
/** acc_gl_account.classification_enum: 2 liability, 5 expense. */
private static final int LIABILITY = 2;
private static final int EXPENSE = 5;
/** acc_gl_account.account_usage 1 - a detail account an entry can be posted to, not a header. */
private static final int DETAIL = 1;
@Transactional
@Override
@@ -66,7 +53,6 @@ public class DdsAccountTypeWritePlatformServiceJpaRepositoryImpl implements DdsA
try {
this.fromApiJsonDeserializer.validateForCreate(command.json());
validateLedgerAccounts(command);
final DdsAccountType accountType = DdsAccountType.fromJson(command);
this.ddsAccountTypeRepository.saveAndFlush(accountType);
@@ -91,7 +77,6 @@ public class DdsAccountTypeWritePlatformServiceJpaRepositoryImpl implements DdsA
try {
this.fromApiJsonDeserializer.validateForUpdate(command.json());
validateLedgerAccounts(command);
final DdsAccountType accountType = this.ddsAccountTypeRepository.findById(accountTypeId)
.orElseThrow(() -> new DdsAccountTypeNotFoundException(accountTypeId));
@@ -115,13 +100,6 @@ public class DdsAccountTypeWritePlatformServiceJpaRepositoryImpl implements DdsA
changesOnly.put("accountsRepriced", openAccounts.size());
}
// A renamed product: the accounts on it - tied to it by id - show the new code.
if (changesOnly.containsKey("accountTypeCode")) {
final int moved = this.jdbcTemplate.update("update m_dds_party set sub_type = ? where account_type_id = ?", // NOSONAR
accountType.getAccountTypeCode(), accountTypeId);
changesOnly.put("accountsRenamed", moved);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(accountTypeId) //
@@ -137,47 +115,6 @@ public class DdsAccountTypeWritePlatformServiceJpaRepositoryImpl implements DdsA
}
}
/**
* Each ledger account a product names must be one an entry can land on, and of the kind its job
* needs - the same rule the savings product's Accounting tab applies by offering only those. The
* customers' money is owed, so a liability; their interest and the agents' commission are costs, so
* expenses.
*/
private void validateLedgerAccounts(final JsonCommand command) {
final List<ApiParameterError> errors = new ArrayList<>();
checkLedgerAccount(command, "depositLiabilityGlId", "Customer Deposits", List.of(LIABILITY), errors);
checkLedgerAccount(command, "interestExpenseGlId", "Interest Expense", List.of(EXPENSE), errors);
checkLedgerAccount(command, "commissionExpenseGlId", "Agent Commission Expense", List.of(EXPENSE), errors);
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
private void checkLedgerAccount(final JsonCommand command, final String parameter, final String label, final List<Integer> kinds,
final List<ApiParameterError> errors) {
final Long glId = command.parameterExists(parameter) ? command.longValueOfParameterNamed(parameter) : null;
if (glId == null) {
return;
}
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(GL_SQL, glId); // NOSONAR
final String code = "error.msg.dds.account.type." + parameter + ".invalid";
if (rows.isEmpty()) {
errors.add(ApiParameterError.parameterError(code, label + ": GL account " + glId + " does not exist.", parameter));
return;
}
final Map<String, Object> gl = rows.get(0);
final String name = gl.get("name") + " (" + gl.get("gl_code") + ")";
if (Boolean.TRUE.equals(gl.get("disabled"))) {
errors.add(ApiParameterError.parameterError(code, label + ": " + name + " is disabled.", parameter));
} else if (((Number) gl.get("account_usage")).intValue() != DETAIL) {
errors.add(ApiParameterError.parameterError(code, label + ": " + name + " is a header account and cannot take entries.",
parameter));
} else if (!kinds.contains(((Number) gl.get("classification_enum")).intValue())) {
errors.add(ApiParameterError.parameterError(code, label + ": " + name + " is not "
+ (kinds.get(0) == LIABILITY ? "a liability" : "an expense") + " account.", parameter));
}
}
private void handleDataIntegrityIssues(final Throwable realCause, final Exception dve) {
if (realCause.getMessage() != null && realCause.getMessage().contains("account_type_code")) {
throw new PlatformDataIntegrityException("error.msg.dds.account.type.duplicate.code",
@@ -43,8 +43,7 @@ public class DdsAccountTypeConfiguration {
@ConditionalOnMissingBean(DdsAccountTypeWritePlatformService.class)
public DdsAccountTypeWritePlatformService ddsAccountTypeWritePlatformService(
DdsAccountTypeCommandFromApiJsonDeserializer fromApiJsonDeserializer, DdsAccountTypeRepository ddsAccountTypeRepository,
PartyRepository partyRepository, JdbcTemplate jdbcTemplate) {
return new DdsAccountTypeWritePlatformServiceJpaRepositoryImpl(fromApiJsonDeserializer, ddsAccountTypeRepository, partyRepository,
jdbcTemplate);
PartyRepository partyRepository) {
return new DdsAccountTypeWritePlatformServiceJpaRepositoryImpl(fromApiJsonDeserializer, ddsAccountTypeRepository, partyRepository);
}
}
@@ -26,12 +26,9 @@ import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.exception.CommandNotAwaitingApprovalException;
import org.apache.fineract.commands.exception.CommandNotFoundException;
import org.apache.fineract.portfolio.dds.accounting.DdsLedger;
import org.apache.fineract.portfolio.dds.accounting.exception.DdsLedgerNotConfiguredException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
@@ -66,7 +63,6 @@ public class DdsApprovalReadService {
private static final String REF_KEY_PREFIX = "DDS_COMMAND:";
private final JdbcTemplate jdbcTemplate;
private final DdsLedger ddsLedger;
public List<Map<String, Object>> list(final String status, final String fromDate, final String toDate) {
final StringBuilder sql = new StringBuilder(ENTRIES_SQL).append(" and c.status = ? ");
@@ -99,423 +95,9 @@ public class DdsApprovalReadService {
final Map<String, Object> row = describe(found.get(0));
row.put("description", text(jsonOf(found.get(0).get("json")), "withdrawDescription"));
row.put("lines", linesFor(row, found.get(0)));
row.put("fields", fieldsFor(row, found.get(0)));
if (isAgentRequest(row)) {
row.put("guarantors", guarantorsOf(jsonOf(found.get(0).get("json"))));
}
if (isPartyRequest(row)) {
final JsonObject partyJson = jsonOf(found.get(0).get("json"));
row.put("nominees", nomineesOf(partyJson));
row.put("nomineeSplit", text(partyJson, "nomineeSplitMode"));
}
return row;
}
/** A DDS account (party) being opened or changed - the entries whose review shows the account and its nominees. */
private static boolean isPartyRequest(final Map<String, Object> row) {
return "PARTY".equals(row.get("entity")) && ("CREATE".equals(row.get("action")) || "UPDATE".equals(row.get("action")));
}
/** What a DDS account request carries, in the order the Party Master Entry screen asks for it. */
private record PartyField(String parameter, String label, String group, String kind) {}
private static final String PARTY_GROUP_CUSTOMER = "Customer & Scheme";
private static final String PARTY_GROUP_PERSONAL = "Personal & Contact";
private static final String PARTY_GROUP_MINOR = "Minor / Guardian";
private static final String PARTY_GROUP_TERMS = "Scheme Terms";
private static final String PARTY_GROUP_STATUS = "Balances & Status";
private static final String PARTY_GROUP_PAYOUT = "Maturity Payout";
private static final List<PartyField> PARTY_FIELDS = List.of( //
new PartyField("clientId", "Client", PARTY_GROUP_CUSTOMER, "client"), //
new PartyField("subType", "Scheme", PARTY_GROUP_CUSTOMER, "scheme"), //
new PartyField("agentId", "Agent", PARTY_GROUP_CUSTOMER, "agent"), //
new PartyField("officeId", "Branch", PARTY_GROUP_CUSTOMER, "office"), //
new PartyField("nominal", "Nominal", PARTY_GROUP_CUSTOMER, "yesno"), //
new PartyField("partyName", "Party Name", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("partyName1", "Party Name 1", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("address1", "Address 1", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("address2", "Address 2", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("address3", "Address 3", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("city", "City", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("pincode", "Pincode", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("gender", "Gender", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("telephone", "Telephone", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("mobileNoSms", "Mobile No (SMS)", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("aadharNo", "Aadhar No", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("panCardNo", "PAN Card No", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("ckycId", "CKYC ID", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("operation", "Operation", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("birthDate", "Birth Date", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("occupation", "Occupation", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("risk", "Risk", PARTY_GROUP_PERSONAL, "text"), //
new PartyField("adultMinor", "Adult / Minor", PARTY_GROUP_MINOR, "text"), //
new PartyField("guardianClientId", "Guardian", PARTY_GROUP_MINOR, "client"), //
new PartyField("relation", "Relation", PARTY_GROUP_MINOR, "text"), //
new PartyField("minorDate", "Comes of Age On", PARTY_GROUP_MINOR, "text"), //
new PartyField("minAccountOpt", "Minor Account Option", PARTY_GROUP_MINOR, "text"), //
new PartyField("openingDate", "Opening Date", PARTY_GROUP_TERMS, "text"), //
new PartyField("dailyDepositAmount", "Daily Deposit", PARTY_GROUP_TERMS, "money"), //
new PartyField("days", "Days", PARTY_GROUP_TERMS, "text"), //
new PartyField("month", "Months", PARTY_GROUP_TERMS, "text"), //
new PartyField("depositAmount", "Deposit Amount", PARTY_GROUP_TERMS, "money"), //
new PartyField("maturityDate", "Maturity Date", PARTY_GROUP_TERMS, "text"), //
new PartyField("ledgerNo", "Ledger No", PARTY_GROUP_TERMS, "text"), //
new PartyField("reopenDate", "Reopen Date", PARTY_GROUP_TERMS, "text"), //
new PartyField("freezeCode", "Freeze Code", PARTY_GROUP_STATUS, "text"), //
new PartyField("lien", "Lien", PARTY_GROUP_STATUS, "yesno"), //
new PartyField("lienReason", "Lien Reason", PARTY_GROUP_STATUS, "text"), //
new PartyField("lienReasonOther", "Lien Reason (Other)", PARTY_GROUP_STATUS, "text"), //
new PartyField("isSmsActive", "SMS Active", PARTY_GROUP_STATUS, "yesno"), //
new PartyField("isEmailActive", "Email Active", PARTY_GROUP_STATUS, "yesno"), //
new PartyField("od1Flag", "OD 1", PARTY_GROUP_STATUS, "yesno"), //
new PartyField("od2Flag", "OD 2", PARTY_GROUP_STATUS, "yesno"), //
new PartyField("payDate", "Pay Date", PARTY_GROUP_STATUS, "text"), //
new PartyField("interestDate", "Interest Date", PARTY_GROUP_STATUS, "text"), //
new PartyField("spDescription", "Description", PARTY_GROUP_STATUS, "text"), //
new PartyField("advCode", "Adv Code", PARTY_GROUP_PAYOUT, "text"), //
new PartyField("creditToBranch", "Credit To Branch", PARTY_GROUP_PAYOUT, "text"), //
new PartyField("creditToAccount", "Credit To A/C", PARTY_GROUP_PAYOUT, "text"), //
new PartyField("creditToAccountType", "Credit To A/C Type", PARTY_GROUP_PAYOUT, "text"));
/**
* A DDS account request laid out for the checker: the client, scheme, agent and branch by name
* rather than by number, in the sections the screen has. Only what the request carries is shown, so
* an edit lists just the fields it changes, and a field left empty is left out.
*/
private List<Map<String, Object>> partyFieldsFor(final JsonObject json) {
final List<Map<String, Object>> fields = new ArrayList<>();
for (final PartyField field : PARTY_FIELDS) {
if (!json.has(field.parameter()) || json.get(field.parameter()).isJsonNull()) {
continue;
}
final String raw = json.get(field.parameter()).getAsString();
if (raw.isBlank()) {
continue;
}
final String value = switch (field.kind()) {
case "client" -> clientLabel(raw);
case "scheme" -> nameOr(first("select account_type_code || ' · ' || account_type_description from m_dds_account_type "
+ "where account_type_code = ?", raw), raw);
case "agent" -> nameOr(first("select agent_code || ' · ' || coalesce(agent_name, '') from m_dds_agent where id = ?",
parseLong(raw)), "Agent #" + raw);
case "office" -> nameOr(first("select name from m_office where id = ?", parseLong(raw)), "Branch #" + raw);
case "yesno" -> Boolean.parseBoolean(raw.trim()) ? "Yes" : "No";
case "money" -> "₹" + plain(raw);
default -> raw.trim();
};
final Map<String, Object> out = new LinkedHashMap<>();
out.put("key", field.parameter());
out.put("label", field.label());
out.put("group", field.group());
out.put("format", "yesno".equals(field.kind()) ? "yesno" : "text");
out.put("value", value);
fields.add(out);
}
return fields;
}
/** The nominees a DDS account request names, with what each is to receive. */
private List<Map<String, Object>> nomineesOf(final JsonObject json) {
final List<Map<String, Object>> nominees = new ArrayList<>();
if (!json.has("nominees") || !json.get("nominees").isJsonArray()) {
return nominees;
}
for (final JsonElement element : json.getAsJsonArray("nominees")) {
if (!element.isJsonObject()) {
continue;
}
final JsonObject n = element.getAsJsonObject();
final Map<String, Object> out = new LinkedHashMap<>();
out.put("name", text(n, "name"));
out.put("relation", text(n, "relation"));
out.put("gender", text(n, "gender"));
out.put("mobileNo", text(n, "mobileNo"));
out.put("birthDate", text(n, "birthDate"));
out.put("nominationDate", text(n, "nominationDate"));
out.put("guardianName", text(n, "guardianName"));
out.put("share", text(n, "sharePercent") != null ? plain(text(n, "sharePercent")) + "%"
: text(n, "sequenceNo") != null ? "Order " + text(n, "sequenceNo") : null);
nominees.add(out);
}
return nominees;
}
/** An agent being opened or changed - the entries whose review shows the agent and their guarantors. */
private static boolean isAgentRequest(final Map<String, Object> row) {
return "AGENT".equals(row.get("entity")) && ("CREATE".equals(row.get("action")) || "UPDATE".equals(row.get("action")));
}
/** What an agent request carries, in the order the Agent Master Entry screen asks for it. */
private record AgentField(String parameter, String label, String group, String kind) {}
private static final String AGENT_GROUP = "Agent";
private static final String AGENT_ADDRESS_GROUP = "Address & Contact";
private static final String AGENT_ACCOUNTS_GROUP = "Branch & Accounts";
private static final String AGENT_BALANCES_GROUP = "Balances";
private static final List<AgentField> AGENT_FIELDS = List.of( //
new AgentField("clientId", "Client", AGENT_GROUP, "client"), //
new AgentField("agentNameId", "Agent Name", AGENT_GROUP, "text"), //
new AgentField("agentBirthDate", "Birth Date", AGENT_GROUP, "text"), //
new AgentField("agentOpeningDate", "Opening Date", AGENT_GROUP, "text"), //
new AgentField("agentAddress1", "Address (Line 1)", AGENT_ADDRESS_GROUP, "text"), //
new AgentField("agentAddress2", "Address (Line 2)", AGENT_ADDRESS_GROUP, "text"), //
new AgentField("agentCity", "City", AGENT_ADDRESS_GROUP, "text"), //
new AgentField("agentPinCode", "PinCode", AGENT_ADDRESS_GROUP, "text"), //
new AgentField("agentTelephone", "Telephone", AGENT_ADDRESS_GROUP, "text"), //
new AgentField("officeId", "Home Branch", AGENT_ACCOUNTS_GROUP, "office"), //
new AgentField("glAccountId", "GL Account", AGENT_ACCOUNTS_GROUP, "gl"), //
new AgentField("agentSavingAccountType", "Saving A/C Type", AGENT_ACCOUNTS_GROUP, "text"), //
new AgentField("agentSavingAccountNo", "Saving A/C No", AGENT_ACCOUNTS_GROUP, "text"), //
new AgentField("agentOpeningBalance", "Opening Balance", AGENT_BALANCES_GROUP, "money"));
/**
* An agent request laid out for the checker: the client, the branch and the ledger account by name
* rather than by number, in the sections the screen has. Only what the request carries is shown, so
* an edit lists just the fields it changes.
*/
private List<Map<String, Object>> agentFieldsFor(final JsonObject json) {
final List<Map<String, Object>> fields = new ArrayList<>();
for (final AgentField field : AGENT_FIELDS) {
if (!json.has(field.parameter()) || json.get(field.parameter()).isJsonNull()) {
continue;
}
final String raw = json.get(field.parameter()).getAsString();
// A new agent starts with nothing, so a zero opening balance is not something to approve and
// is left out. One that is not zero - an edit that sets it - is shown.
if ("money".equals(field.kind()) && isZero(raw)) {
continue;
}
final String value = switch (field.kind()) {
case "client" -> clientLabel(raw);
case "office" -> nameOr(first("select name from m_office where id = ?", parseLong(raw)), "Branch #" + raw);
case "gl" -> nameOr(first("select gl_code || ' · ' || name from acc_gl_account where id = ?", parseLong(raw)),
"Account #" + raw);
case "money" -> "₹" + plain(raw);
default -> raw.trim();
};
final Map<String, Object> out = new LinkedHashMap<>();
out.put("key", field.parameter());
out.put("label", field.label());
out.put("group", field.group());
out.put("format", "text");
out.put("value", value);
fields.add(out);
}
return fields;
}
/** The guarantors an agent request names, each with the client they are by number and name. */
private List<Map<String, Object>> guarantorsOf(final JsonObject json) {
final List<Map<String, Object>> guarantors = new ArrayList<>();
if (!json.has("guarantors") || !json.get("guarantors").isJsonArray()) {
return guarantors;
}
for (final JsonElement element : json.getAsJsonArray("guarantors")) {
if (!element.isJsonObject()) {
continue;
}
final JsonObject g = element.getAsJsonObject();
final Map<String, Object> out = new LinkedHashMap<>();
out.put("client", text(g, "clientId") == null ? null : clientLabel(text(g, "clientId")));
out.put("name", text(g, "guarantorName"));
out.put("telephone", text(g, "telephone"));
out.put("address", joined(", ", text(g, "address1"), text(g, "address2"), text(g, "city"), text(g, "pinCode")));
guarantors.add(out);
}
return guarantors;
}
/**
* An agent's code, name, client, branch, savings account and guarantor count. An agent already on
* the books is read as they stand; one still being opened does not exist yet, so is read from the
* request and has no code until approval gives it one.
*/
private Map<String, Object> agentColumns(final Long agentId, final JsonObject json) {
final Map<String, Object> columns = new LinkedHashMap<>();
if (agentId != null) {
columns.putAll(this.jdbcTemplate.queryForList( // NOSONAR
"select a.agent_code as \"agentCode\", a.agent_name as \"agentName\", c.display_name as \"clientName\", "
+ "o.name as \"officeName\", a.agent_saving_account_no as \"savingAccountNo\", "
+ "(select count(*) from m_dds_agent_guarantor g where g.agent_id = a.id) as \"guarantorCount\" "
+ "from m_dds_agent a left join m_client c on c.id = a.client_id left join m_office o on o.id = a.office_id "
+ "where a.id = ?",
agentId).stream().findFirst().orElse(Map.of()));
return columns;
}
final String clientId = text(json, "clientId");
columns.put("agentCode", null);
columns.put("agentName", text(json, "agentNameId"));
columns.put("clientName", clientId == null ? null : first("select display_name from m_client where id = ?", parseLong(clientId)));
final String officeId = text(json, "officeId");
columns.put("officeName", officeId == null ? null : first("select name from m_office where id = ?", parseLong(officeId)));
columns.put("savingAccountNo", text(json, "agentSavingAccountNo"));
columns.put("guarantorCount", json.has("guarantors") && json.get("guarantors").isJsonArray() ? json.getAsJsonArray("guarantors").size() : 0);
return columns;
}
private String clientLabel(final String clientId) {
final String name = first("select display_name from m_client where id = ?", parseLong(clientId));
return name == null ? "Client #" + clientId : clientId + " · " + name;
}
/** The name an agent request gives, or the client it is for when none was typed. */
private String agentNameOf(final JsonObject json) {
final String name = text(json, "agentNameId");
if (name != null && !name.isBlank()) {
return name.trim();
}
final String clientId = text(json, "clientId");
return clientId == null ? "" : clientLabel(clientId);
}
private static boolean isZero(final String amount) {
try {
return new BigDecimal(amount.trim()).signum() == 0;
} catch (NumberFormatException e) {
return false;
}
}
private static String nameOr(final String found, final String fallback) {
return found == null ? fallback : found;
}
private static String joined(final String separator, final String... parts) {
final List<String> present = new ArrayList<>();
for (final String part : parts) {
if (part != null && !part.isBlank()) {
present.add(part.trim());
}
}
return String.join(separator, present);
}
/** A number an agent request carried as text, or -1 - which no row has - when it was not one. */
private static Long parseLong(final String value) {
try {
return Long.valueOf(value.trim());
} catch (NumberFormatException e) {
return -1L;
}
}
/**
* A DDS product's terms, in the order the Account Type screen asks for them: the column it is
* stored in, the parameter the request carries it as, its label, how it reads, and the section of
* the review it is shown in.
*/
private record ProductField(String column, String parameter, String label, String format, String group) {}
private static final String GROUP_PRODUCT = "Product";
private static final String GROUP_INTEREST = "Interest";
private static final String GROUP_CLOSURE = "Maturity & Premature Closure";
private static final String GROUP_ACCOUNTING = "Accounting";
private static final List<ProductField> PRODUCT_FIELDS = List.of( //
new ProductField("account_type_group", "accountTypeGroup", "Account Type Group", "text", GROUP_PRODUCT), //
new ProductField("account_type_code", "accountTypeCode", "Account Type Code", "text", GROUP_PRODUCT), //
new ProductField("account_type_description", "accountTypeDescription", "Description", "text", GROUP_PRODUCT), //
new ProductField("is_active", "active", "Active", "yesno", GROUP_PRODUCT), //
new ProductField("interest_rate", "interestRate", "Interest Rate", "percent", GROUP_INTEREST), //
new ProductField("auto_interest_post", "autoInterestPost", "Auto Interest Post", "yesno", GROUP_INTEREST), //
new ProductField("auto_interest_post_when", "autoInterestPostWhen", "Auto Interest Post When", "text", GROUP_INTEREST), //
new ProductField("maturity_auto_close", "maturityAutoClose", "Maturity Auto Close", "yesno", GROUP_CLOSURE), //
new ProductField("premature_lockin_months", "prematureLockinMonths", "Premature Lock-in", "months", GROUP_CLOSURE), //
new ProductField("premature_interest_reduction", "prematureInterestReduction", "Premature Interest Reduction", "percent",
GROUP_CLOSURE), //
new ProductField("premature_agent_commission_penalty", "prematureAgentCommissionPenalty", "Agent Commission Penalty",
"percent", GROUP_CLOSURE), //
new ProductField("deposit_liability_gl_id", "depositLiabilityGlId", "DDS Customer Deposits (Liability)", "gl",
GROUP_ACCOUNTING), //
new ProductField("interest_expense_gl_id", "interestExpenseGlId", "Interest Expense", "gl", GROUP_ACCOUNTING), //
new ProductField("commission_expense_gl_id", "commissionExpenseGlId", "Agent Commission Expense", "gl", GROUP_ACCOUNTING));
/**
* What an entry that moves no money actually asks for, field by field, so the checker can read the
* terms they are approving rather than only a code.
*
* An edit still waiting is shown against the product as it stands, with each changed field marked,
* because what matters there is what will change. Once actioned, the product has already moved on,
* so only the values the request carried are shown.
*/
private List<Map<String, Object>> fieldsFor(final Map<String, Object> row, final Map<String, Object> entry) {
if (isAgentRequest(row)) {
return agentFieldsFor(jsonOf(entry.get("json")));
}
if (isPartyRequest(row)) {
return partyFieldsFor(jsonOf(entry.get("json")));
}
if (!"DDSACCOUNTTYPE".equals(row.get("entity"))) {
return List.of();
}
final JsonObject json = jsonOf(entry.get("json"));
final Long resourceId = (Long) row.get("resourceId");
final boolean compare = "UPDATE".equals(row.get("action")) && "PENDING".equals(row.get("status")) && resourceId != null;
final Map<String, Object> current = compare
? this.jdbcTemplate.queryForList("select * from m_dds_account_type where id = ?", resourceId).stream().findFirst() // NOSONAR
.orElse(Map.of())
: Map.of();
final List<Map<String, Object>> fields = new ArrayList<>();
for (final ProductField field : PRODUCT_FIELDS) {
final boolean requested = json.has(field.parameter()) && !json.get(field.parameter()).isJsonNull();
final String proposed = requested ? display(json.get(field.parameter()).getAsString(), field.format()) : null;
final Map<String, Object> out = new LinkedHashMap<>();
out.put("key", field.parameter());
out.put("label", field.label());
out.put("group", field.group());
out.put("format", field.format());
if (compare) {
final Object stored = current.get(field.column());
final String was = stored == null ? null : display(String.valueOf(stored), field.format());
final String now = requested ? proposed : was;
out.put("current", was);
out.put("value", now);
out.put("changed", requested && !Objects.equals(was, now));
} else if (requested) {
out.put("value", proposed);
} else {
continue;
}
fields.add(out);
}
return fields;
}
/** As format, and a ledger account id read as the account itself: "DDS CUSTOMER DEPOSITS (200601)". */
private String display(final String value, final String format) {
if (!"gl".equals(format) || value == null || value.isBlank()) {
return format(value, format);
}
final Map<String, Object> gl = this.jdbcTemplate.queryForList("select gl_code, name from acc_gl_account where id = ?", // NOSONAR
parseLong(value)).stream().findFirst().orElse(Map.of());
return gl.isEmpty() ? "GL " + value.trim() + " (not found)" : gl.get("name") + " (" + gl.get("gl_code") + ")";
}
/** A stored or requested value as the screen shows it: Yes/No, 2%, 12 months. */
private static String format(final String value, final String format) {
if (value == null || value.isBlank()) {
return null;
}
return switch (format) {
case "yesno" -> Boolean.parseBoolean(value.trim()) ? "Yes" : "No";
case "percent" -> plain(value) + "%";
case "months" -> plain(value) + " month" + ("1".equals(plain(value)) ? "" : "s");
default -> value.trim();
};
}
/** 2.000000 reads as 2, and 1.50 as 1.5, so a stored rate and a typed one compare equal. */
private static String plain(final String value) {
try {
return new BigDecimal(value.trim()).stripTrailingZeros().toPlainString();
} catch (NumberFormatException e) {
return value.trim();
}
}
/**
* The debit and credit sides of the entry, as its voucher shows them. Once the money has moved they
* are the voucher's own lines; while it waits they are what approving it will move, worked out from
@@ -578,49 +160,13 @@ public class DdsApprovalReadService {
}
}
case "DDSBATCH:POST" -> {
// The expense side, on the accounts of the DDS products concerned - interest on each
// customer's product's interest account, commission shared over the commission accounts
// of the products each agent collected for, as the posting itself will book it.
final String jobType = first("select job_type from m_dds_batch_run where id = ?", resourceId);
// A calculation books nothing itself; its lines are what the matching Posting run will
// book, shown so the checker sees which accounts and ledger accounts the figures belong to.
final boolean projected = jobType != null && jobType.endsWith("_CALCULATION");
if (jobType != null && jobType.startsWith("COMMISSION")) {
final Map<String, BigDecimal> byAccount = new LinkedHashMap<>();
final Map<String, String> names = new LinkedHashMap<>();
for (final Map<String, Object> runRow : this.jdbcTemplate.queryForList( // NOSONAR
"select agent_id, amount, to_date from m_dds_batch_run_row where batch_run_id = ? and amount > 0 order by id",
resourceId)) {
try {
final long agentOfRow = ((Number) runRow.get("agent_id")).longValue();
final BigDecimal amountOfRow = (BigDecimal) runRow.get("amount");
(projected && runRow.get("to_date") != null
? this.ddsLedger.projectedCommissionSplit(agentOfRow, amountOfRow,
((java.sql.Date) runRow.get("to_date")).toLocalDate())
: this.ddsLedger.commissionSplit(agentOfRow, amountOfRow, resourceId))
.forEach((account, share) -> {
byAccount.merge(account.getGlCode(), share, BigDecimal::add);
names.put(account.getGlCode(), account.getName());
});
} catch (final DdsLedgerNotConfiguredException e) {
byAccount.merge("", (BigDecimal) runRow.get("amount"), BigDecimal::add);
names.put("", "Agent Commission Expense not set on the DDS product");
}
}
byAccount.forEach((code, share) -> lines.add(line("DR", code.isEmpty() ? null : code, names.get(code), "GL", share)));
} else {
for (final Map<String, Object> gl : this.jdbcTemplate.queryForList( // NOSONAR
"select g.gl_code as \"code\", coalesce(g.name, 'Interest Expense not set on DDS product ' || "
+ "upper(trim(p.sub_type))) as \"name\", sum(r.amount) as \"amount\" from m_dds_batch_run_row r "
+ "join m_dds_party p on p.id = r.party_id "
+ "left join m_dds_account_type t on t.account_type_code = upper(trim(p.sub_type)) "
+ "left join acc_gl_account g on g.id = t.interest_expense_gl_id "
+ "where r.batch_run_id = ? and r.amount > 0 group by g.gl_code, g.name, upper(trim(p.sub_type)) "
+ "order by g.gl_code",
resourceId)) {
lines.add(line("DR", (String) gl.get("code"), (String) gl.get("name"), "GL", (BigDecimal) gl.get("amount")));
}
}
final String expense = first("select c.name from m_dds_batch_run r join c_configuration c on c.name = "
+ "case when r.job_type like 'COMMISSION%' then 'dds-agent-commission-expense-account' "
+ "else 'dds-interest-expense-account' end where r.id = ?", resourceId);
final Map<String, Object> gl = expense == null ? Map.of()
: this.jdbcTemplate.queryForList("select g.gl_code as \"code\", g.name from c_configuration c " // NOSONAR
+ "join acc_gl_account g on g.id = c.value where c.name = ?", expense).stream().findFirst().orElse(Map.of());
lines.add(line("DR", (String) gl.get("code"), (String) gl.get("name"), "GL", amount));
for (final Map<String, Object> credited : this.jdbcTemplate.queryForList( // NOSONAR
"select coalesce(p.account_no, a.agent_saving_account_no) as \"accountNo\", "
+ "coalesce(p.party_code || ' · ' || p.party_name, a.agent_code || ' · ' || a.agent_name) as \"accountName\", "
@@ -631,10 +177,6 @@ public class DdsApprovalReadService {
lines.add(line("CR", (String) credited.get("accountNo"), (String) credited.get("accountName"),
(String) credited.get("type"), (BigDecimal) credited.get("amount")));
}
if (projected) {
// Marked, so the review says these entries are made by the Posting run, not on approval.
lines.forEach(each -> each.put("projected", true));
}
}
default -> {
// A change to a party, agent or scheme moves no money and has no sides.
@@ -772,15 +314,11 @@ public class DdsApprovalReadService {
case "CLOSE" -> "Close Agent";
default -> "Edit Agent";
};
details = resourceId != null ? agent(resourceId) : agentNameOf(json);
details = resourceId != null ? agent(resourceId) : String.valueOf(text(json, "agentName"));
}
case "DDSACCOUNTTYPE:CREATE", "DDSACCOUNTTYPE:UPDATE" -> {
type = "CREATE".equals(action) ? "New DDS Product" : "Edit DDS Product";
details = text(json, "accountTypeCode") != null ? text(json, "accountTypeCode") : scheme(resourceId);
final String description = text(json, "accountTypeDescription");
if (description != null && !description.isBlank()) {
details = details == null || details.isBlank() ? description : details + " · " + description;
}
}
case "COMMISSIONSLAB:CREATE", "COMMISSIONSLAB:UPDATE" -> {
type = "CREATE".equals(action) ? "New Commission Slab" : "Edit Commission Slab";
@@ -804,14 +342,6 @@ public class DdsApprovalReadService {
row.put("type", type);
row.put("details", details);
row.put("amount", amount);
if ("PARTY".equals(entity) && ("CREATE".equals(action) || "UPDATE".equals(action))) {
// The account in the columns Approvals -> Pending Account lists every other account by.
row.putAll(accountColumns(resourceId, json));
}
if ("AGENT".equals(entity)) {
// The agent in the columns Approvals -> Agent Approval lists them by.
row.putAll(agentColumns(resourceId, json));
}
final Long commandId = ((Number) entry.get("id")).longValue();
final String voucherNo = switch (status) {
case STATUS_PROCESSED -> voucherFor(commandId, entity, action, resourceId, json, (String) entry.get("madeOn"));
@@ -898,37 +428,6 @@ public class DdsApprovalReadService {
return found == null ? "Agent #" + agentId : found.trim();
}
/**
* A DDS account's number, holder, scheme, branch and agent. An edit reads the account as it
* stands; a new account does not exist until approved, so it is read from the request, and has
* no number yet.
*/
private Map<String, Object> accountColumns(final Long partyId, final JsonObject json) {
final Map<String, Object> columns = new LinkedHashMap<>();
if (partyId != null) {
final Map<String, Object> stored = this.jdbcTemplate.queryForList( // NOSONAR
"select p.account_no as \"accountNo\", p.party_name as \"clientName\", p.sub_type as \"subType\", "
+ "o.name as \"officeName\", trim(a.agent_code || ' ' || coalesce(a.agent_name, '')) as \"agentName\" "
+ "from m_dds_party p left join m_office o on o.id = p.office_id "
+ "left join m_dds_agent a on a.id = p.agent_id where p.id = ?",
partyId).stream().findFirst().orElse(Map.of());
columns.putAll(stored);
} else {
columns.put("accountNo", null);
columns.put("clientName", text(json, "partyName"));
columns.put("subType", text(json, "subType"));
final Long officeId = longOf(json, "officeId");
columns.put("officeName", officeId == null ? null : first("select name from m_office where id = ?", officeId));
final Long agentId = longOf(json, "agentId");
columns.put("agentName", agentId == null ? null : agent(agentId));
}
final String subType = (String) columns.remove("subType");
final String description = subType == null ? null
: first("select account_type_description from m_dds_account_type where account_type_code = ?", subType);
columns.put("productName", description == null ? subType : subType + " · " + description);
return columns;
}
private String scheme(final Long accountTypeId) {
return accountTypeId == null ? "" : first("select account_type_code from m_dds_account_type where id = ?", accountTypeId);
}
@@ -22,7 +22,6 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
@@ -39,8 +38,6 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.dds.batch.data.BatchRunData;
import org.apache.fineract.portfolio.dds.batch.service.BatchRunReadPlatformService;
import org.apache.fineract.portfolio.dds.batch.service.BatchRunWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.springframework.stereotype.Component;
@Path("/v1/dds/batchruns")
@@ -55,20 +52,6 @@ public class BatchRunsApiResource {
private final BatchRunReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final DdsVoucherService ddsVoucherService;
private final BatchRunWritePlatformService writePlatformService;
@DELETE
@Path("{batchRunId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Discard a previewed DDS Batch Run", description = "Deletes a run that was generated but never posted - "
+ "the preview the screen's Discard button throws away. Refused for a run that is posted, posting, failed, or whose "
+ "posting is waiting for approval.")
public CommandProcessingResult discard(@PathParam("batchRunId") @Parameter(description = "batchRunId") final Long batchRunId) {
// Whoever may generate a preview may throw one away; it moved nothing.
this.context.authenticatedUser().validateHasPermissionTo("GENERATE_" + RESOURCE_NAME_FOR_PERMISSIONS);
this.writePlatformService.discardBatchRun(batchRunId);
return new CommandProcessingResultBuilder().withEntityId(batchRunId).build();
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@@ -30,12 +30,6 @@ public interface BatchRunWritePlatformService {
CommandProcessingResult postBatchRun(Long batchRunId, JsonCommand command);
/**
* Throws away a run that was only previewed - generated and never posted, nor waiting for a
* checker to approve its posting. Nothing was applied by generating it, so nothing is undone.
*/
void discardBatchRun(Long batchRunId);
/**
* Posts interest for every scheme whose Account Type says to do it automatically and whose posting
* day falls on the given date. Driven by the Post DDS Interest scheduled job.
@@ -29,8 +29,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
@@ -85,7 +83,6 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
* re-running it for the same period finds no days left; a posting takes its amount off the parked
* figure, so re-posting finds nothing left to move.
*/
@Slf4j
@RequiredArgsConstructor
public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWritePlatformService {
@@ -144,13 +141,9 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
private static final String COMMISSION_ACCOUNT_SQL = "select a.id, a.status_enum, a.product_id, p.deposit_type_enum "
+ "from m_savings_account a join m_savings_product p on p.id = a.product_id where a.account_no = ?";
/**
* Whether a savings account is a DDS customer's scheme account - one a DDS party is linked to.
* Read off the parties themselves rather than a configured product id: what makes an account a
* scheme account is that a scheme runs on it, whatever product it was opened under.
*/
private static final String DDS_SCHEME_ACCOUNT_SQL = "select count(*) from m_dds_party p "
+ "join m_savings_account a on a.id = p.savings_account_id where a.account_no = ?";
/** The product DDS opens customers' scheme accounts under - see 0492. */
private static final String DDS_DEPOSIT_PRODUCT_SQL = "select value from c_configuration "
+ "where name = 'dds-deposit-product' and enabled = true";
private static final int SAVINGS_DEPOSIT_TYPE = 100;
private static final int FIXED_DEPOSIT_TYPE = 200;
@@ -196,9 +189,6 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
default -> throw new IllegalStateException("Unsupported DDS batch job type: " + jobType);
}
// A new preview of the same job replaces the one before it: generating twice used to leave two
// identical "not posted" runs on the list, and nobody could tell which one to post.
discardSupersededPreviews(jobType, subType);
this.batchRunRepository.saveAndFlush(batchRun);
return new CommandProcessingResultBuilder() //
@@ -207,35 +197,6 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
.build();
}
/**
* A run that is only a preview: calculated, never posted, not posting or failed, and with no
* posting of it waiting for a checker (a held posting names the run, and approving it later must
* find it). Only such a run is ever deleted - generating applies nothing, so nothing is lost.
*/
private static final String PREVIEW_ONLY = "r.status = 'CALCULATED' and not exists (select 1 from m_portfolio_command_source c "
+ "where c.entity_name = 'DDSBATCH' and c.resource_id = r.id and c.status = 2)";
private void discardSupersededPreviews(final String jobType, final String subType) {
final String sameJob = "r.job_type = ? and coalesce(r.sub_type, '') = coalesce(?, '') and " + PREVIEW_ONLY;
this.jdbcTemplate.update("delete from m_dds_batch_run_row where batch_run_id in (select r.id from m_dds_batch_run r where " // NOSONAR
+ sameJob + ")", jobType, subType);
this.jdbcTemplate.update("delete from m_dds_batch_run r where " + sameJob, jobType, subType); // NOSONAR
}
@Transactional
@Override
public void discardBatchRun(final Long batchRunId) {
final List<Long> found = this.jdbcTemplate.queryForList("select r.id from m_dds_batch_run r where r.id = ? and " + PREVIEW_ONLY, // NOSONAR
Long.class, batchRunId);
if (found.isEmpty()) {
throw new PlatformDataIntegrityException("error.msg.dds.batch.run.not.discardable",
"Batch run #" + batchRunId + " cannot be discarded: only a run that was generated and not posted (or sent for"
+ " posting approval) can be.");
}
this.jdbcTemplate.update("delete from m_dds_batch_run_row where batch_run_id = ?", batchRunId); // NOSONAR
this.jdbcTemplate.update("delete from m_dds_batch_run where id = ?", batchRunId); // NOSONAR
}
/**
* Parses the "Branch" checklist selection - null/empty means no filter ("All"). Matched against
* Agent.homeBranch by name, since that field is plain text (not a real Office FK) mirroring the
@@ -635,47 +596,26 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
final String jobType = batchRun.getJobType();
final LocalDate jobDate = batchRun.getJobDate();
// Only rows that actually moved money are counted, and their sum kept beside the run's total: a
// posting row is passed over when another run has already paid what it lists, and the run must
// not read as though it paid it again.
int done = 0;
BigDecimal moved = BigDecimal.ZERO;
for (final BatchRunRow row : batchRun.getRows()) {
final boolean applied = switch (jobType) {
case INTEREST_CALCULATION -> {
accrue(row, jobDate, false);
yield true;
}
case OD_INTEREST_CALCULATION -> {
accrue(row, jobDate, true);
yield true;
}
switch (jobType) {
case INTEREST_CALCULATION -> accrue(row, jobDate, false);
case OD_INTEREST_CALCULATION -> accrue(row, jobDate, true);
case INTEREST_POSTING -> postInterest(row, batchRun, false);
case OD_INTEREST_POSTING -> postInterest(row, batchRun, true);
case COMMISSION_CALCULATION -> {
accrueCommission(row, batchRun);
yield true;
}
case COMMISSION_CALCULATION -> accrueCommission(row, batchRun);
case COMMISSION_POSTING -> payCommission(row, batchRun);
case MATURITY_AUTO_CLOSE -> {
closeAtMaturity(row);
yield true;
}
case MATURITY_AUTO_CLOSE -> closeAtMaturity(row);
default -> throw new IllegalStateException("Unsupported DDS batch job type: " + jobType);
};
if (applied) {
done++;
moved = moved.add(row.getAmount() == null ? BigDecimal.ZERO : row.getAmount());
} else {
log.warn("DDS batch run #{}: row {} passed over - what it lists was already paid", batchRun.getId(), row.getId());
}
done++;
}
// Only at the end. A crediting run is one transaction from start to finish, so a count written
// part-way through would be invisible outside it until the run commits - and writing it from a
// transaction of its own would have two of them contending for the same row. The status says
// POSTING while it works, which is what a clerk actually needs to know.
batchRun.recordPosted(done, moved);
batchRun.recordProgress(done);
}
/**
@@ -722,10 +662,10 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
* same accrued interest would each list it in full, and paying both would credit interest that
* was never earned.
*/
private boolean postInterest(final BatchRunRow row, final BatchRun batchRun, final boolean odInterest) {
private void postInterest(final BatchRunRow row, final BatchRun batchRun, final boolean odInterest) {
final Party party = partyOf(row.getPartyId());
if (party.accruedInterest(odInterest).compareTo(row.getAmount()) < 0) {
return false;
return;
}
final BigDecimal newBalance = party.postInterest(row.getAmount(), odInterest);
@@ -738,7 +678,6 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
final String transactionType = odInterest ? "OD_INTEREST_POST" : "INTEREST_POST";
this.partyTransactionRepository.save(new PartyTransaction(party, party.getAgent(), batchRun.getJobDate(), transactionType,
DIRECTION_CREDIT, null, null, null, row.getAmount(), null, null, "DDS batch run #" + batchRun.getId(), newBalance));
return true;
}
/**
@@ -777,12 +716,12 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
* customers and not yet handed to a branch, which commission has nothing to do with. The entry is
* recorded against the balance as it stands, so the ledger reads consistently.
*/
private boolean payCommission(final BatchRunRow row, final BatchRun batchRun) {
private void payCommission(final BatchRunRow row, final BatchRun batchRun) {
final Agent agent = agentOf(row.getAgentId());
// Only what is actually owed can be paid: two posting runs generated over the same owed
// figure would each list it in full, and paying both would pay commission twice.
if (agent.getCommission().compareTo(row.getAmount()) < 0) {
return false;
return;
}
agent.payOutCommission(row.getAmount());
@@ -791,7 +730,6 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
this.partyTransactionRepository.save(new PartyTransaction(null, agent, batchRun.getJobDate(), "COMMISSION_POST", DIRECTION_CREDIT,
null, null, agent.getAgentSavingAccountNo(), row.getAmount(), null, null, "DDS batch run #" + batchRun.getId(),
agent.getAgentCurrentBalance()));
return true;
}
/**
@@ -835,8 +773,8 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
+ " Set an ordinary savings account for this agent");
}
final Integer schemes = this.jdbcTemplate.queryForObject(DDS_SCHEME_ACCOUNT_SQL, Integer.class, accountNo); // NOSONAR
if (schemes != null && schemes > 0) {
final List<Long> ddsProduct = this.jdbcTemplate.queryForList(DDS_DEPOSIT_PRODUCT_SQL, Long.class); // NOSONAR
if (!ddsProduct.isEmpty() && productId.equals(ddsProduct.get(0))) {
throw new AgentCommissionAccountUnusableException(agent.getAgentCode(), "account " + accountNo
+ " is a DDS scheme account belonging to a customer, so commission paid into it would be counted as their"
+ " deposits. Set an ordinary savings account for this agent");
@@ -849,12 +787,8 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
"account " + accountNo + " is " + statusName(status) + " rather than active, so it cannot take a deposit yet");
}
// Each product the agent collected for books its share on its own commission account; the
// collections the share was worked out on are then marked as paid for.
final Map<GLAccount, BigDecimal> shares = this.ddsLedger.commissionSplit(agent.getId(), amount, batchRun.getId());
this.ddsLedger.payCommission(((Number) account.get("id")).longValue(), batchRun.getJobDate(), amount,
"DDS commission, batch run #" + batchRun.getId(), shares);
this.ddsLedger.markCommissionPaid(agent.getId(), batchRun.getId());
"DDS commission, batch run #" + batchRun.getId());
}
/** Fineract's savings statuses, named the way the screens name them. */
@@ -876,36 +810,28 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
// ---------------------------------------------------------------------------------------------
/**
* Posts interest for every scheme whose posting period ended yesterday.
* Posts interest for every scheme that is due today.
*
* A scheme that posts automatically has decided it needs no review, so this does both halves in
* one go: it calculates the interest since the last run and then posts it into the customers'
* balances. Both halves go through the same code the screen's Post button uses, and both leave a
* run behind, so an automatic posting is as visible and as auditable as a manual one.
*
* Interest posts at the end of the period - the last day of the month, quarter, half-year or
* year - as it does on savings and fixed deposits, so there is no posting day to configure and
* no month whose length can trip one up. The job runs just after midnight, so it looks back at
* the day that has just closed: posting on the 1st through the last day of the previous month
* takes in everything collected on that last day, which a run at 00:20 on the day itself would
* not yet have seen.
*
* Runs are scoped to one scheme at a time, because the posting frequency belongs to the scheme.
* Runs are scoped to one scheme at a time, because the posting day belongs to the scheme.
*/
@Transactional
@Override
public int postDueAutoInterest(final LocalDate businessDate) {
final LocalDate periodEnd = businessDate.minusDays(1);
int posted = 0;
for (final DdsAccountType accountType : this.ddsAccountTypeRepository.findDueForAutoInterestPost()) {
if (!isPeriodEnd(accountType, periodEnd)) {
if (!isDueOn(accountType, businessDate)) {
continue;
}
final String subType = accountType.getAccountTypeCode();
calculateAndPost(INTEREST_CALCULATION, periodEnd, subType);
calculateAndPost(INTEREST_POSTING, periodEnd, subType);
calculateAndPost(INTEREST_CALCULATION, businessDate, subType);
calculateAndPost(INTEREST_POSTING, businessDate, subType);
posted++;
}
@@ -913,11 +839,19 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
}
/**
* Whether the given date closes one of this scheme's posting periods: the last day of a month
* the scheme's frequency posts in. February's end is the 28th or 29th, whichever the year has.
* Whether this scheme's posting day falls on the given date.
*
* A day beyond the length of the month means the end of that month: a scheme set to 31 posts on
* 31 January, 28 February and 30 April. Without that, February would simply be skipped and the
* account would lose a month's interest every year.
*/
private boolean isPeriodEnd(final DdsAccountType accountType, final LocalDate date) {
return monthIsDue(accountType.getAutoInterestPostWhen(), date) && date.getDayOfMonth() == date.lengthOfMonth();
private boolean isDueOn(final DdsAccountType accountType, final LocalDate date) {
if (!monthIsDue(accountType.getAutoInterestPostWhen(), date)) {
return false;
}
final int configuredDay = accountType.getAutoInterestPostDay();
final int effectiveDay = Math.min(configuredDay, date.lengthOfMonth());
return date.getDayOfMonth() == effectiveDay;
}
/**
@@ -115,7 +115,6 @@ public final class CollectionEntryCommandFromApiJsonDeserializer {
.inMinMaxRange(1, DdsSchedule.CATCH_UP_DAYS);
} else {
v.reset().parameter(ENTRIES).parameterAtIndexArray("amount", i).value(decimalOf(row)).notNull().positiveAmount();
toThePaisa(v, decimalOf(row));
}
final String mode = stringOf(row, "transactionMode");
@@ -202,14 +201,4 @@ public final class CollectionEntryCommandFromApiJsonDeserializer {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
/**
* Money is kept to the paisa, as a savings account keeps it: 100.555 is refused rather than stored
* on the passbook and the journal as typed.
*/
private static void toThePaisa(final DataValidatorBuilder v, final BigDecimal amount) {
if (amount != null && amount.stripTrailingZeros().scale() > 2) {
v.failWithCodeAndMessage("more.than.two.decimals", "An amount can have at most two decimal places (paise).", amount);
}
}
}
@@ -49,7 +49,6 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte
import org.apache.fineract.organisation.agent.domain.Agent;
import org.apache.fineract.organisation.agent.domain.AgentRepository;
import org.apache.fineract.organisation.agent.exception.AgentNotFoundException;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.dds.collection.domain.CollectionEntry;
import org.apache.fineract.portfolio.dds.collection.domain.CollectionEntryRepository;
import org.apache.fineract.portfolio.dds.collection.exception.CollectionEntryNotFoundException;
@@ -337,54 +336,32 @@ public class CollectionEntryWritePlatformServiceJpaRepositoryImpl implements Col
}
final String description = "DDS collection by " + agent.getAgentCode() + ", voucher " + String.join(", ", voucherNos);
// What the customers on DDS's own books are now owed, by the customer-deposits account of their
// product and the branch they bank at - two products owe on two accounts, and a branch's DDS
// Customer Deposits must add up to its own customers' balances.
final String journalId = "DDSR-A" + applicationId;
final Map<String, BigDecimal> pooled = new LinkedHashMap<>();
final Map<String, GLAccount> depositAccounts = new LinkedHashMap<>();
final Map<String, Office> offices = new LinkedHashMap<>();
BigDecimal pooled = BigDecimal.ZERO;
for (final CollectionEntry entry : pending) {
final Party party = collectableParty(entry.getParty().getPartyCode(), agent);
final boolean pooledEntry = party.getSavingsAccountId() == null;
if (!pooledEntry) {
if (party.getSavingsAccountId() != null) {
this.ddsLedger.applyAgentReceiptToSavings(party, settledOn, entry.getAmount(), receiptAccount, description);
} else {
final GLAccount deposits = this.ddsLedger.depositLiabilityOf(party);
final Office office = this.ddsLedger.officeOf(party);
final String key = office.getId() + ":" + deposits.getId();
depositAccounts.putIfAbsent(key, deposits);
offices.putIfAbsent(key, office);
pooled.merge(key, entry.getAmount(), BigDecimal::add);
pooled = pooled.add(entry.getAmount());
}
// The customer's own statement records the day the money was collected, as it always has.
// The agent's cash in hand is not raised: this cash was counted in at the counter, not
// carried.
final BigDecimal balanceAfter = party.credit(entry.getAmount());
final PartyTransaction line = new PartyTransaction(party, agent, entry.getEntryDate(), "PARTY_TRAN", "CREDIT",
entry.getTransactionMode(), "GL", receiptAccount.getGlCode(), entry.getAmount(), entry.getChequeNo(),
entry.getTokenNo(), description, balanceAfter);
// A pooled line's journal is written after the loop, so it is named here rather than taken
// from whatever the operation wrote last.
final PartyTransaction transaction = this.partyTransactionRepository
.saveAndFlush(pooledEntry ? line.linkJournal(journalId) : line);
final PartyTransaction transaction = this.partyTransactionRepository.saveAndFlush(new PartyTransaction(party, agent,
entry.getEntryDate(), "PARTY_TRAN", "CREDIT", entry.getTransactionMode(), "GL", receiptAccount.getGlCode(),
entry.getAmount(), entry.getChequeNo(), entry.getTokenNo(), description, balanceAfter));
entry.markSettledByReceipt(settledOn, transaction.getId(), applicationId);
this.collectionEntryRepository.save(entry);
}
// Everyone on DDS's own accounts, in one pair per branch and customer-deposits account, all under one journal id.
boolean journalled = false;
for (final Map.Entry<String, BigDecimal> owed : pooled.entrySet()) {
if (owed.getValue().signum() > 0) {
this.ddsLedger.applyAgentReceipts(offices.get(owed.getKey()), settledOn, owed.getValue(), receiptAccount,
depositAccounts.get(owed.getKey()), journalId, description);
journalled = true;
}
}
if (journalled) {
// Everyone on DDS's own accounts, in one pair.
final String journalId = "DDSR-A" + applicationId;
if (pooled.signum() > 0) {
this.ddsLedger.applyAgentReceipts(agent.getOffice(), settledOn, pooled, receiptAccount, journalId, description);
this.jdbcTemplate.update(SET_JOURNAL_SQL, journalId, applicationId); // NOSONAR
}
@@ -110,23 +110,6 @@ public class PartiesApiResource {
return readPlatformService.retrieveCreditableAccounts(clientId);
}
@GET
@Path("nextcode")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "What the Party Master Entry screen shows before saving",
description = "partyCode: the code the next DDS party will be given - worked out the way saving does it (PT and one "
+ "more than the highest number on the books), but only an estimate: another party saved first, or one "
+ "waiting for approval, can take it. lastAccountNo: the number of the DDS account opened most recently, "
+ "null when there is none. Both are for display; nothing is saved from them.")
public Map<String, Object> retrieveNextPartyCode() {
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
final Map<String, Object> result = new java.util.LinkedHashMap<>();
result.put("partyCode", readPlatformService.nextPartyCode());
result.put("lastAccountNo", readPlatformService.lastAccountNo());
return result;
}
@GET
@Path("sundryaccounts")
@Consumes({ MediaType.APPLICATION_JSON })
@@ -35,19 +35,6 @@ public interface PartyReadPlatformService {
*/
Collection<Map<String, Object>> retrieveCreditableAccounts(Long clientId);
/**
* The code the next party saved will be given, for a screen to show before it is saved. It is worked
* out the way saving does it - "PT" and one more than the highest number on the books - but only an
* estimate: another party saved first, or one waiting for approval, can take it.
*/
String nextPartyCode();
/**
* The account number of the DDS account opened most recently, or null when none has been - what the
* Party Master Entry screen shows as Last A/c. Read only: nothing is saved from it.
*/
String lastAccountNo();
/**
* How a closed account was closed - early or at maturity, by whom, what the balance came to and
* where it went - gathered from its transactions, the ledger and the command that closed it.
@@ -104,10 +104,7 @@ public class PartyReadPlatformServiceImpl implements PartyReadPlatformService {
+ " p.freeze_code as freezeCode, p.adv_code as advCode, p.credit_to_branch as creditToBranch, "
+ " p.credit_to_account_type as creditToAccountType, p.credit_to_account as creditToAccount, "
+ " p.nominee_applicable as nomineeApplicable, p.nominee_split_mode as nomineeSplitMode, "
+ " p.status as status, p.close_date as closeDate, p.account_type_id as accountTypeId, "
+ " p.currency_code as currencyCode, p.activatedon_date as activatedOnDate, "
+ " (select u.username from m_appuser u where u.id = p.activatedon_userid) as activatedByUsername, "
+ " (select u.username from m_appuser u where u.id = p.closedon_userid) as closedByUsername "
+ " p.status as status, p.close_date as closeDate "
// Left join: sub_type is a plain code column rather than a foreign key, so a party
// whose account type was renamed away or predates the master still lists, just
// without a description.
@@ -217,11 +214,6 @@ public class PartyReadPlatformServiceImpl implements PartyReadPlatformService {
.nomineeSplitMode(rs.getString("nomineeSplitMode")) //
.status(rs.getString("status")) //
.closeDate(JdbcSupport.getLocalDate(rs, "closeDate")) //
.accountTypeId(JdbcSupport.getLong(rs, "accountTypeId")) //
.currencyCode(rs.getString("currencyCode")) //
.activatedOnDate(JdbcSupport.getLocalDate(rs, "activatedOnDate")) //
.activatedByUsername(rs.getString("activatedByUsername")) //
.closedByUsername(rs.getString("closedByUsername")) //
.nominees(new ArrayList<>()) //
.build();
}
@@ -334,32 +326,14 @@ public class PartyReadPlatformServiceImpl implements PartyReadPlatformService {
+ "p.name as \"productName\", a.account_balance_derived as balance, 'SAVINGS' as \"accountKind\" from m_savings_account a "
+ "join m_savings_product p on p.id = a.product_id "
+ "where a.client_id = ? and a.status_enum = 300 and p.deposit_type_enum = 100 "
+ "and not exists (select 1 from m_dds_party dp where dp.savings_account_id = a.id) "
+ "and a.product_id is distinct from (select cast(value as bigint) from c_configuration "
+ "where name = 'dds-deposit-product' and enabled = true) "
+ "union all "
+ "select c.id as id, c.account_no as \"accountNo\", cp.name as \"productName\", c.account_balance_derived as balance, "
+ "'CURRENT' as \"accountKind\" from m_current_account c join m_current_product cp on cp.id = c.product_id "
+ "where c.client_id = ? and c.status_enum = 300"
+ ") accounts order by \"accountNo\"";
@Override
public String nextPartyCode() {
// The same figure saving a party works from - the highest number among every code on the books,
// closed parties included - so what a screen shows is what saving would give it right now.
final Long highest = this.jdbcTemplate.queryForObject( // NOSONAR
"select coalesce(max(cast(substring(party_code, 3) as bigint)), 0) from m_dds_party where party_code like 'PT%'",
Long.class);
return String.format("PT%04d", (highest == null ? 0L : highest) + 1L);
}
@Override
public String lastAccountNo() {
// The highest number on the DDS account serial - the account opened last. Closed accounts count:
// this says what was opened, not what is still open.
final List<String> found = this.jdbcTemplate.queryForList( // NOSONAR
"select account_no from m_dds_party where account_no is not null order by account_no desc limit 1", String.class);
return found.isEmpty() ? null : found.get(0);
}
@Override
public Collection<Map<String, Object>> retrieveCreditableAccounts(final Long clientId) {
return this.jdbcTemplate.queryForList(CREDITABLE_ACCOUNTS_SQL, clientId, clientId); // NOSONAR
@@ -29,7 +29,6 @@ import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -80,10 +79,10 @@ import org.apache.fineract.portfolio.dds.party.exception.PartySubTypeNotActiveEx
import org.apache.fineract.portfolio.dds.party.serialization.PartyCommandFromApiJsonDeserializer;
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransaction;
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransactionRepository;
import org.apache.fineract.portfolio.savings.data.SavingsAccountDataDTO;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
import org.apache.fineract.portfolio.savings.service.SavingsApplicationProcessWritePlatformService;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.orm.jpa.JpaSystemException;
@@ -135,20 +134,15 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
final String partyCode = generateNextPartyCode();
final Party party = Party.fromJson(client, agent, officeNamed(command.longValueOfParameterNamed(OFFICE_ID)), partyCode,
command);
final DdsAccountType scheme = schemeOf(party);
party.takeSchemeRate(scheme.getInterestRate());
party.linkProduct(scheme.getId());
// Operative from the day it is opened - by whoever executes this, the checker where the
// opening went through maker-checker - in the bank's currency.
party.recordActivation(organisationCurrency(), DateUtils.getBusinessLocalDate(), currentUserId());
party.takeSchemeRate(schemeOf(party).getInterestRate());
linkSavingsAccountIfProvided(party, clientId, command);
addNomineesFromCommand(party, command);
// The party is the DDS account: nothing is opened in the savings tables for it. Its number
// is the next on the DDS account serial, taken in this transaction - so an opening that
// fails or is rolled back hands its number back, and the numbers run without gaps.
party.assignAccountNo(nextAccountSerial());
// The party is the DDS account: nothing is opened in the savings tables for it. Its
// number comes from its id, so it is given once the first save has made one.
this.partyRepository.saveAndFlush(party);
party.assignAccountNo();
this.partyRepository.saveAndFlush(party);
return new CommandProcessingResultBuilder() //
@@ -173,10 +167,6 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
this.fromApiJsonDeserializer.validateForUpdate(command.json());
final Party partyForUpdate = this.partyRepository.findById(partyId).orElseThrow(() -> new PartyNotFoundException(partyId));
refuseEditOfClosedAccount(partyForUpdate);
// Asked before the update changes anything: is the branch this request names a different one?
final boolean officeMoves = command.parameterExists(OFFICE_ID) && (partyForUpdate.getOffice() == null
|| !partyForUpdate.getOffice().getId().equals(command.longValueOfParameterNamed(OFFICE_ID)));
// Checked against whoever owns the party once this update lands, not who owns it now -
// moving the party to another customer and naming the old one's account in the same save
@@ -186,16 +176,13 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
validateCreditToAccount(owningClientId, command);
final Map<String, Object> changesOnly = partyForUpdate.update(command);
refuseChangeOfLockedTerms(partyForUpdate, changesOnly, officeMoves);
// Only when it actually changed: a party already sitting on a since-retired account type
// must stay editable for everything else.
if (changesOnly.containsKey("subType")) {
validateSubType((String) changesOnly.get("subType"));
// Moved to another scheme: it takes that scheme's rate, and is tied to it by id.
final DdsAccountType newScheme = schemeOf(partyForUpdate);
partyForUpdate.takeSchemeRate(newScheme.getInterestRate());
partyForUpdate.linkProduct(newScheme.getId());
// Moved to another scheme: it takes that scheme's rate.
partyForUpdate.takeSchemeRate(schemeOf(partyForUpdate).getInterestRate());
changesOnly.put("interestRate", partyForUpdate.getInterestRate());
}
@@ -258,7 +245,7 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
}
final LocalDate closeDate = command.localDateValueOfParameterNamed("closeDate");
party.close(closeDate, currentUserId());
party.close(closeDate);
this.partyRepository.saveAndFlush(party);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(partyId).build();
@@ -360,7 +347,7 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
// open only leaves a second step to forget - an account that reads as active in every report and
// that an agent could still collect on. The same as Maturity Auto Close does, on the day the
// money moved.
party.close(DateUtils.getBusinessLocalDate(), currentUserId());
party.close(DateUtils.getBusinessLocalDate());
this.partyRepository.saveAndFlush(party);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(partyId).build();
@@ -458,7 +445,7 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
// The balance is nil now, which is the one thing closing insists on. Closed on the business
// date rather than the maturity date: this is the day the money actually moved, and a close
// date earlier than its own payout would read as though the account had been shut first.
party.close(DateUtils.getBusinessLocalDate(), currentUserId());
party.close(DateUtils.getBusinessLocalDate());
this.partyRepository.saveAndFlush(party);
return new CommandProcessingResultBuilder().withEntityId(partyId).build();
@@ -691,8 +678,9 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
// Sending one maturity into another moves money between two collection accounts instead of
// handing it back, and if that scheme has already closed the money is stranded in an account
// nobody can operate. The same refusal an agent's commission account gets.
final Integer schemes = this.jdbcTemplate.queryForObject(DDS_SCHEME_ACCOUNT_SQL, Integer.class, accountNo); // NOSONAR
if (schemes != null && schemes > 0) {
final Long productId = ((Number) account.get("product_id")).longValue();
final List<Long> ddsProduct = this.jdbcTemplate.queryForList(DEPOSIT_PRODUCT_SQL, Long.class); // NOSONAR
if (!ddsProduct.isEmpty() && productId.equals(ddsProduct.get(0))) {
throw new PartyCreditAccountUnusableException(accountNo, "it is a DDS scheme account, not an account to be paid into");
}
@@ -717,13 +705,9 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
}
}
/**
* Whether a savings account is a DDS customer's scheme account - one a DDS party is linked to.
* Read off the parties themselves rather than a configured product id: what makes an account a
* scheme account is that a scheme runs on it, whatever product it was opened under.
*/
private static final String DDS_SCHEME_ACCOUNT_SQL = "select count(*) from m_dds_party p "
+ "join m_savings_account a on a.id = p.savings_account_id where a.account_no = ?";
/** Set by 0492. Names the savings product a DDS customer's real account is opened under. */
private static final String DEPOSIT_PRODUCT_SQL = "select value from c_configuration "
+ "where name = 'dds-deposit-product' and enabled = true";
/**
* Opens the customer's real deposit account, unless one was named and linked above.
@@ -740,10 +724,22 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
return;
}
// DDS keeps its customers' accounts itself (0514), with each DDS product's own ledger accounts
// behind them, and no savings product is named for DDS any more - so there is no savings
// account to open. A party's balance is already on the books through its product.
throw new DdsDepositProductNotConfiguredException();
final List<Long> productIds = this.jdbcTemplate.queryForList(DEPOSIT_PRODUCT_SQL, Long.class); // NOSONAR
if (productIds.isEmpty() || productIds.get(0) == null) {
throw new DdsDepositProductNotConfiguredException();
}
final SavingsAccountDataDTO accountDetails = new SavingsAccountDataDTO(client, null, productIds.get(0), party.getOpeningDate(),
this.context.authenticatedUser(), DateUtils.DEFAULT_DATE_FORMATTER);
final CommandProcessingResult result = this.savingsApplicationProcessWritePlatformService.createActiveApplication(accountDetails);
final Long savingsAccountId = result.getSavingsId() != null ? result.getSavingsId() : result.getResourceId();
party.linkSavingsAccount(savingsAccountId);
// Withdrawals are blocked from the moment the account exists. A scheme account is a savings
// account like any other, and left open a customer could empty a twelve-month scheme at the
// counter the next day - which is the one thing daily deposit saving exists to prevent.
// Deposits are untouched; only the way out is closed, and DDS lifts it for its own payouts.
this.ddsLedger.lockDebits(savingsAccountId);
}
/**
@@ -754,68 +750,6 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
* counted at another. Refuses an id that names no branch rather than leaving the account
* unplaced.
*/
/**
* Who is doing this, for the account's own record of it. A background run has no logged-in user;
* it is recorded as the system administrator, as the audit columns record it (AuditorAwareImpl).
*/
private Long currentUserId() {
final AppUser user = this.context.getAuthenticatedUserIfPresent();
return user != null ? user.getId() : SYSTEM_ADMIN_USER_ID;
}
private static final Long SYSTEM_ADMIN_USER_ID = 1L;
/** The bank's currency - a DDS account is opened in it. */
private String organisationCurrency() {
final List<String> codes = this.jdbcTemplate.queryForList("select code from m_organisation_currency order by id limit 1", // NOSONAR
String.class);
return codes.isEmpty() ? null : codes.get(0);
}
/**
* What an active DDS account's scheme is built on, and so cannot change while it runs - as a savings
* account's product, currency and opening cannot: the customer and product, the branch whose books
* carry it, the opening and its balance, and the deposit terms (daily amount, deposit, days, months)
* that its maturity amount and interest are worked out from, with the dates and amounts that follow
* from them. Changing one mid-scheme would leave the balance, the interest already posted and the
* maturity figure answering to terms the account no longer has. The collecting agent is locked too:
* the agent's pending collections, commission and cash in hand are counted against the account.
* Contact details, nominees, lien and freeze, SMS/e-mail and where the maturity is paid stay editable.
*/
private static final List<String> LOCKED_WHEN_ACTIVE = List.of("clientId", "subType", "agentId", "openingDate", "openingBalance",
"dailyDepositAmount", "depositAmount", "days", "month", "interestDate", "payDate", "paymentAmount", "reopenDate",
"lastAccount");
private void refuseEditOfClosedAccount(final Party party) {
if (party.isClosed()) {
final String message = "DDS account " + party.getAccountNo() + " is closed and cannot be edited.";
throw new PlatformApiDataValidationException("error.msg.dds.party.closed.not.editable", message,
List.of(ApiParameterError.parameterError("error.msg.dds.party.closed.not.editable", message, "status")));
}
}
private void refuseChangeOfLockedTerms(final Party party, final Map<String, Object> changes, final boolean officeMoves) {
if (!Party.STATUS_ACTIVE.equals(party.getStatus())) {
return;
}
final List<ApiParameterError> errors = new ArrayList<>();
for (final String field : LOCKED_WHEN_ACTIVE) {
if (changes.containsKey(field)) {
errors.add(ApiParameterError.parameterError("error.msg.dds.party.term.locked." + field,
"'" + field + "' cannot be changed on an active DDS account.", field));
}
}
if (officeMoves) {
errors.add(ApiParameterError.parameterError("error.msg.dds.party.term.locked." + OFFICE_ID,
"The branch cannot be changed on an active DDS account.", OFFICE_ID));
}
if (!errors.isEmpty()) {
// Thrown inside the update's transaction, so nothing the request changed is kept.
throw new PlatformApiDataValidationException("error.msg.dds.party.terms.locked",
"These cannot be changed on an active DDS account: " + errors.size() + " field(s).", errors);
}
}
private Office officeNamed(final Long officeId) {
return this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
}
@@ -950,7 +884,7 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
payOutRemainder(party, command, closeDate, lockinOverrideReason);
party.close(closeDate, currentUserId());
party.close(closeDate);
this.partyRepository.saveAndFlush(party);
final Map<String, Object> changes = new HashMap<>();
@@ -1136,20 +1070,6 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
}
}
/**
* The next DDS account number. The update locks the serial's one row until this transaction ends,
* so two openings at once queue on it rather than taking the same number, and a rollback undoes the
* increment - unlike a sequence, which never gives a number back.
*/
private long nextAccountSerial() {
final Long next = this.jdbcTemplate.queryForObject( // NOSONAR
"update m_dds_account_number_serial set last_value = last_value + 1 where id = 1 returning last_value", Long.class);
if (next == null) {
throw new IllegalStateException("m_dds_account_number_serial has no row - migration 0624 has not run");
}
return next;
}
private String generateNextPartyCode() {
final Long maxSuffix = this.partyRepository.retrieveMaxPartyCodeSuffix();
final long nextSuffix = (maxSuffix == null ? 0L : maxSuffix) + 1L;
@@ -22,7 +22,6 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
@@ -162,7 +161,6 @@ public final class PartyTransactionCommandFromApiJsonDeserializer {
} else {
v.reset().parameter(COLLECTIONS).parameterAtIndexArray("amount", i).value(extractBigDecimal(row, "amount"))
.notNull().positiveAmount();
toThePaisa(v, extractBigDecimal(row, "amount"));
}
}
}
@@ -173,11 +171,9 @@ public final class PartyTransactionCommandFromApiJsonDeserializer {
.notBlank();
v.reset().parameter("withdrawAmount").value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("withdrawAmount", element))
.notNull().positiveAmount();
toThePaisa(v, this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("withdrawAmount", element));
} else if ("AGENT_TRAN".equals(transactionType)) {
v.reset().parameter("amount").value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element)).notNull()
.positiveAmount();
toThePaisa(v, this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element));
// An agent handing its round's takings to the branch is a counter transaction like any
// other, so it says how the money arrived on the same terms a collection does.
@@ -211,14 +207,4 @@ public final class PartyTransactionCommandFromApiJsonDeserializer {
dataValidationErrors);
}
}
/**
* Money is kept to the paisa, as a savings account keeps it: 100.555 is refused rather than stored
* on the passbook and the journal as typed.
*/
private static void toThePaisa(final DataValidatorBuilder v, final BigDecimal amount) {
if (amount != null && amount.stripTrailingZeros().scale() > 2) {
v.failWithCodeAndMessage("more.than.two.decimals", "An amount can have at most two decimal places (paise).", amount);
}
}
}
@@ -45,13 +45,9 @@ public class PartyTransactionReadPlatformServiceImpl implements PartyTransaction
+ " t.agent_id as agentId, a.agent_code as agentCode, t.transaction_date as transactionDate, "
+ " t.transaction_type as transactionType, t.direction as direction, t.transaction_mode as transactionMode, "
+ " t.account_type as accountType, t.account_no as accountNo, t.amount as amount, t.cheque_no as chequeNo, "
+ " t.token_no as tokenNo, t.description as description, t.balance_after_transaction as balanceAfterTransaction, t.voucher_no as voucherNo, "
+ " t.office_id as officeId, o.name as officeName, t.submitted_on_date as submittedOnDate, "
+ " t.journal_transaction_id as journalTransactionId, t.ref_no as refNo, "
+ " u.username as createdByUsername, t.created_date as createdDate, a.agent_name as agentName "
+ " t.token_no as tokenNo, t.description as description, t.balance_after_transaction as balanceAfterTransaction, t.voucher_no as voucherNo "
+ " from m_dds_party_transaction t left join m_dds_party p on p.id = t.party_id "
+ " join m_dds_agent a on a.id = t.agent_id left join m_office o on o.id = t.office_id "
+ " left join m_appuser u on u.id = t.createdby_id";
+ " join m_dds_agent a on a.id = t.agent_id";
}
@Override
@@ -75,14 +71,6 @@ public class PartyTransactionReadPlatformServiceImpl implements PartyTransaction
.description(rs.getString("description")) //
.balanceAfterTransaction(rs.getBigDecimal("balanceAfterTransaction")) //
.voucherNo(rs.getString("voucherNo")) //
.officeId(JdbcSupport.getLong(rs, "officeId")) //
.officeName(rs.getString("officeName")) //
.submittedOnDate(JdbcSupport.getLocalDate(rs, "submittedOnDate")) //
.journalTransactionId(rs.getString("journalTransactionId")) //
.refNo(rs.getString("refNo")) //
.createdByUsername(rs.getString("createdByUsername")) //
.createdDate(rs.getTimestamp("createdDate") == null ? null : rs.getTimestamp("createdDate").toLocalDateTime()) //
.agentName(rs.getString("agentName")) //
.build();
}
}
@@ -53,6 +53,7 @@ import org.apache.fineract.portfolio.dds.party.exception.PartyNotFoundException;
import org.apache.fineract.portfolio.dds.transaction.domain.DdsTransactionMode;
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransaction;
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransactionRepository;
import org.apache.fineract.portfolio.dds.transaction.exception.InsufficientAgentBalanceException;
import org.apache.fineract.portfolio.dds.transaction.exception.PartyAdvanceAmountMismatchException;
import org.apache.fineract.portfolio.dds.transaction.exception.PartyArrearsDaysExceededException;
import org.apache.fineract.portfolio.dds.transaction.exception.PartyHasNoDailyAmountException;
@@ -275,12 +276,28 @@ public class PartyTransactionWritePlatformServiceJpaRepositoryImpl implements Pa
// receipt tagged DDS into the agent's own GL account - and applied to the customers from Agent
// Tran > Apply DDS Receipt. Taking the same cash in here as well would be a second way in for
// one bag of notes, so this way is closed, and says where the right one is.
throw new PlatformDataIntegrityException("error.msg.dds.agent.tran.moved.to.cashier",
"An agent's cash is no longer taken in here. The cashier receipts it at Cashier > Transaction Entry "
+ "(Receipt, Cash, GL, Entry Type DDS, A/C No " + (agent != null && agent.getGlAccount() != null
? agent.getGlAccount().getGlCode()
: "the agent's GL account") + "), and it is applied from Daily Transaction Detail > Agent Tran > "
+ "Apply DDS Receipt.");
if (agent != null) {
throw new PlatformDataIntegrityException("error.msg.dds.agent.tran.moved.to.cashier",
"An agent's cash is no longer taken in here. The cashier receipts it at Cashier > Transaction Entry "
+ "(Receipt, Cash, GL, Entry Type DDS, A/C No " + (agent.getGlAccount() != null ? agent.getGlAccount().getGlCode()
: "the agent's GL account") + "), and it is applied from Daily Transaction Detail > Agent Tran > "
+ "Apply DDS Receipt.");
}
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("amount");
if (agent.getAgentCurrentBalance().compareTo(amount) < 0) {
throw new InsufficientAgentBalanceException(agent.getAgentCode());
}
final BigDecimal newBalance = agent.settle(amount);
// The cash reaches the branch. No customer is involved - they were credited when their money
// was collected; this is the bank catching up with where the notes actually are.
this.ddsLedger.settle(agent.getOffice(), transactionDate, amount, "DDS settlement by " + agent.getAgentCode(), agent.getGlAccount());
return new PartyTransaction(null, agent, transactionDate, "AGENT_TRAN", "DEBIT",
DdsTransactionMode.canonicalOrOriginal(command.stringValueOfParameterNamedAllowingNull("transactionMode")),
command.stringValueOfParameterNamedAllowingNull("accountType"),
command.stringValueOfParameterNamedAllowingNull("accountNo"), amount, null, null,
command.stringValueOfParameterNamedAllowingNull("description"), newBalance);
}
private List<PartyTransaction> postWithdrawDeposite(final Agent agent, final LocalDate transactionDate,
@@ -0,0 +1,81 @@
/**
* 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.investment.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
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.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.investment.data.InvestmentAmortisationData;
import org.apache.fineract.portfolio.investment.data.InvestmentAmortisationDueData;
import org.apache.fineract.portfolio.investment.service.InvestmentAmortisationService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/amortisation")
@Component
@Tag(name = "Investment Premium Amortisation", description = "Premium of HTM holdings amortised straight-line to maturity every 31 March.")
@RequiredArgsConstructor
public class InvestmentAmortisationApiResource {
private final PlatformSecurityContext context;
private final InvestmentAmortisationService amortisationService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Path("{purchaseId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Amortisation schedule of a holding (06 Amortize Entry)")
public InvestmentAmortisationData schedule(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentSaleApiConstants.ENTITY);
return this.amortisationService.schedule(purchaseId);
}
@GET
@Path("due")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Holdings due for amortisation on a 31 March (07 Amortize Yearly Process)", description = "fyEnd dd-MM-yyyy")
public List<InvestmentAmortisationDueData> due(@QueryParam("fyEnd") @Parameter(description = "dd-MM-yyyy") final String fyEnd) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentSaleApiConstants.ENTITY);
return this.amortisationService.due(InvestmentApiDates.parse(fyEnd, null, "financial year end"));
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Post amortisation for a 31 March", description = "With purchaseId: that holding (Amortize Entry); without: "
+ "every holding due (Amortize Yearly Process). One JV per holding: Dr 500604 / Cr premium GL.")
public CommandProcessingResult amortise(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().amortiseInvestmentPremium().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,151 @@
/**
* 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.investment.api;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Constants for the Treasury Investment masters (Bank / Party Master and Script Master), modelled on the bank's existing
* CBS Investment screens.
*/
public final class InvestmentApiConstants {
private InvestmentApiConstants() {}
public static final String ENTITY_PARTY = "INVESTMENTPARTY";
public static final String ENTITY_SCRIPT = "INVESTMENTSCRIPT";
public static final String RESOURCE_PARTY = "investmentparty";
public static final String RESOURCE_SCRIPT = "investmentscript";
/** Instrument ("Bond Type") an investment party / script belongs to. */
public static final Set<String> BOND_TYPES = Set.of("GOVT_SECURITY", "NON_SLR", "BOND", "MUTUAL_FUND", "FD", "SHARE");
/** Bond types that carry coupon terms (ISIN, interest rate, issue date are mandatory for these). */
public static final Set<String> DEBT_BOND_TYPES = Set.of("GOVT_SECURITY", "NON_SLR", "BOND");
/** SLR classification on the Script Master ("GOI" = Central Government). */
public static final Set<String> SLR_TYPES = Set.of("GOI", "STATE_GOVT", "TREASURY", "NON_SLR");
/** Script Code prefix per bond type, e.g. G00001 for the first Govt Security. */
public static final Map<String, String> SCRIPT_CODE_PREFIX = Map.of("GOVT_SECURITY", "G", "NON_SLR", "N", "BOND", "B", "MUTUAL_FUND",
"M", "FD", "F", "SHARE", "S");
/** Bond types that are non-SLR investments, counted against the RBI limit of 10% of deposits. */
public static final Set<String> NON_SLR_BOND_TYPES = Set.of("NON_SLR", "BOND", "MUTUAL_FUND");
/** Bond types that must carry a credit rating: corporate bonds, CPs and CDs. */
public static final Set<String> RATED_BOND_TYPES = Set.of("NON_SLR", "BOND");
/** Credit ratings on the Script Master: long-term (bonds) and short-term (CP / CD) scales, best first. */
public static final List<String> RATINGS = List.of("AAA", "AA+", "AA", "AA-", "A+", "A", "A-", "BBB+", "BBB", "BBB-", "BB+", "BB",
"BB-", "B+", "B", "B-", "C", "D", "A1+", "A1", "A2+", "A2", "A3+", "A3", "A4+", "A4", "UNRATED");
/** RBI: corporate bonds rated "A" or higher, CPs and CDs rated "A2" or higher. */
public static final Set<String> ELIGIBLE_RATINGS = Set.of("AAA", "AA+", "AA", "AA-", "A+", "A", "A-", "A1+", "A1", "A2+", "A2");
/** Short-term scale, for CPs and CDs (issued for up to one year); bonds carry the long-term scale. */
public static final Set<String> SHORT_TERM_RATINGS = Set.of("A1+", "A1", "A2+", "A2", "A3+", "A3", "A4+", "A4");
/** Credit rating agencies registered with SEBI. */
public static final List<String> RATING_AGENCIES = List.of("CRISIL", "ICRA", "CARE", "INDIA_RATINGS", "ACUITE", "BRICKWORK",
"INFOMERICS");
/** RBI: only debt and money-market mutual funds; their sub types start with this. */
public static final String DEBT_MF_SUB_TYPE_PREFIX = "DEBT";
/** Share investments: only these institutions (no shares of companies outside the co-operative sector). */
public static final String SHARE = "SHARE";
public static final String INSTITUTION_STATE_COOP_BANK = "STATE_COOP_BANK";
public static final String INSTITUTION_DCCB = "DCCB";
public static final String INSTITUTION_OTHER_COOP_SOCIETY = "OTHER_COOP_SOCIETY";
/** Membership shares of NPCI, CCIL, SWIFT, the Umbrella Organisation of UCBs and IDPIC. */
public static final String INSTITUTION_MEMBERSHIP_MIC_UO = "MEMBERSHIP_MIC_UO";
public static final Set<String> INSTITUTION_TYPES = Set.of(INSTITUTION_STATE_COOP_BANK, INSTITUTION_DCCB, INSTITUTION_OTHER_COOP_SOCIETY,
INSTITUTION_MEMBERSHIP_MIC_UO);
/** Co-operative institutions: their shares are valued on the dividend / balance sheet rules. */
public static final Set<String> COOP_INSTITUTION_TYPES = Set.of(INSTITUTION_STATE_COOP_BANK, INSTITUTION_DCCB,
INSTITUTION_OTHER_COOP_SOCIETY);
public static final String PARTY_CODE_PREFIX = "PTY-";
/**
* Deal slip: how the deal was confirmed to the counterparty (entered with the deal), and the counterparty's
* confirmation the back office records when it settles the deal.
*/
public static final Set<String> CONFIRMATION_MODES = Set.of("EMAIL", "NDS_OM", "FAX", "LETTER", "PHONE");
public static final String CONFIRMATION_MODE = "confirmationMode";
public static final String CONFIRMATION_RECEIVED_ON = "confirmationReceivedOn";
public static final String CONFIRMATION_REF = "confirmationRef";
// Common parameters
public static final String BOND_TYPE = "bondType";
public static final String GL_ACCOUNT_ID = "glAccountId";
public static final String INTEREST_GL_ACCOUNT_ID = "interestGlAccountId";
public static final String ADDRESS1 = "address1";
public static final String ADDRESS2 = "address2";
public static final String ADDRESS3 = "address3";
public static final String CITY = "city";
public static final String PINCODE = "pincode";
public static final String PHONE = "phone";
public static final String FAX = "fax";
public static final String ACTIVE = "active";
public static final String LOCALE = "locale";
public static final String DATE_FORMAT = "dateFormat";
// Party parameters
public static final String PARTY_NAME = "partyName";
public static final String BANK_TYPE = "bankType";
public static final String RBI_CODE = "rbiCode";
public static final String EXPOSURE_LIMIT = "limit";
public static final String LEI_NO = "leiNo";
public static final String CP_BANK_NAME = "cpBankName";
public static final String CP_BRANCH_NAME = "cpBranchName";
public static final String CP_ACCOUNT_NO = "cpAccountNo";
public static final String CP_IFSC = "cpIfsc";
public static final String TDS_APPLICABLE = "tdsApplicable";
public static final String TDS_RATE = "tdsRate";
public static final String INSTITUTION_TYPE = "institutionType";
public static final String SUBSCRIBED_CAPITAL = "subscribedCapital";
public static final String LAST_BALANCE_SHEET_DATE = "lastBalanceSheetDate";
public static final String IN_LIQUIDATION = "inLiquidation";
// Script parameters
public static final String SLR_TYPE = "slrType";
public static final String SCRIPT_DESC = "scriptDesc";
public static final String ISIN = "isin";
public static final String FACE_VALUE_PER_UNIT = "faceValuePerUnit";
public static final String INT_RATE = "intRate";
public static final String ISSUE_DATE = "issueDate";
public static final String MATURITY_DATE = "maturityDate";
public static final String COMPANY_NAME = "companyName";
public static final String CORP_CODE = "corpCode";
public static final String RATING = "rating";
public static final String RATING_AGENCY = "ratingAgency";
public static final String LISTED = "listed";
public static final Set<String> PARTY_PARAMS = Set.of(BOND_TYPE, PARTY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX,
BANK_TYPE, GL_ACCOUNT_ID, INTEREST_GL_ACCOUNT_ID, RBI_CODE, EXPOSURE_LIMIT, LEI_NO, CP_BANK_NAME, CP_BRANCH_NAME, CP_ACCOUNT_NO, CP_IFSC, ACTIVE,
TDS_APPLICABLE, TDS_RATE, INSTITUTION_TYPE, SUBSCRIBED_CAPITAL, LAST_BALANCE_SHEET_DATE, IN_LIQUIDATION, LOCALE, DATE_FORMAT);
public static final Set<String> SCRIPT_PARAMS = Set.of(BOND_TYPE, SLR_TYPE, SCRIPT_DESC, GL_ACCOUNT_ID, ISIN, FACE_VALUE_PER_UNIT,
INT_RATE, ISSUE_DATE, MATURITY_DATE, COMPANY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX, CORP_CODE,
RATING, RATING_AGENCY, LISTED, ACTIVE, LOCALE, DATE_FORMAT);
}
@@ -0,0 +1,50 @@
/**
* 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.investment.api;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
/** Query-parameter dates of the Investment APIs: DD-MM-YYYY, strictly (31-02 is refused, not moved to month end). */
public final class InvestmentApiDates {
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-uuuu").withResolverStyle(ResolverStyle.STRICT);
private InvestmentApiDates() {}
/** The date in {@code value}, or {@code fallback} when it is blank; a malformed date is a clear 403, not a 500. */
public static LocalDate parse(final String value, final LocalDate fallback, final String name) {
if (value == null || value.isBlank()) {
if (fallback == null) {
throw new GeneralPlatformDomainRuleException("error.msg.investment.date.required", "Enter the " + name + " as DD-MM-YYYY",
name);
}
return fallback;
}
try {
return LocalDate.parse(value.trim(), DATE);
} catch (final DateTimeParseException e) {
throw new GeneralPlatformDomainRuleException("error.msg.investment.date.invalid", "Enter the " + name + " as DD-MM-YYYY",
value);
}
}
}
@@ -0,0 +1,72 @@
/**
* 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.investment.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.investment.data.InvestmentBucketReportData;
import org.apache.fineract.portfolio.investment.service.InvestmentBucketReportService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/bucket-report")
@Component
@Tag(name = "Investment Bucket Wise", description = "Investments held on a date, split by the time left to maturity.")
@RequiredArgsConstructor
public class InvestmentBucketReportApiResource {
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-uuuu").withResolverStyle(ResolverStyle.STRICT);
private final PlatformSecurityContext context;
private final InvestmentBucketReportService reportService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Bucket Wise Investment", description = "asOnDate dd-MM-yyyy; default today")
public InvestmentBucketReportData report(@QueryParam("asOnDate") @Parameter(description = "dd-MM-yyyy") final String asOnDate) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
if (asOnDate == null || asOnDate.isBlank()) {
return this.reportService.report(DateUtils.getBusinessLocalDate());
}
final LocalDate asOn;
try {
asOn = LocalDate.parse(asOnDate.trim(), DATE);
} catch (final DateTimeParseException e) {
throw new GeneralPlatformDomainRuleException("error.msg.investment.report.date.invalid", "Enter the date as DD-MM-YYYY",
asOnDate);
}
if (asOn.isAfter(DateUtils.getBusinessLocalDate())) {
throw new GeneralPlatformDomainRuleException("error.msg.investment.report.future", "The as-on date cannot be after today");
}
return this.reportService.report(asOn);
}
}
@@ -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.investment.api;
import java.util.Set;
/** Constants for the Daily Script Master (market rate of a script for a day). */
public final class InvestmentDailyRateApiConstants {
private InvestmentDailyRateApiConstants() {}
public static final String ENTITY = "INVESTMENTDAILYRATE";
public static final String RESOURCE = "investmentdailyrate";
public static final String SCRIPT_ID = "scriptId";
public static final String RATE_DATE = "rateDate";
public static final String OPEN_RATE = "openRate";
public static final String HIGH_RATE = "highRate";
public static final String LOW_RATE = "lowRate";
public static final String CLOSE_RATE = "closeRate";
public static final String LOCALE = "locale";
public static final String DATE_FORMAT = "dateFormat";
public static final Set<String> PARAMS = Set.of(SCRIPT_ID, RATE_DATE, OPEN_RATE, HIGH_RATE, LOW_RATE, CLOSE_RATE, LOCALE, DATE_FORMAT);
}
@@ -0,0 +1,93 @@
/**
* 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.investment.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
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.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.investment.data.InvestmentDailyRateData;
import org.apache.fineract.portfolio.investment.service.InvestmentDailyRateReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/daily-rates")
@Component
@Tag(name = "Investment Daily Script Master", description = "Market rate of each script for a day; Purchase Entry takes its rate from here.")
@RequiredArgsConstructor
public class InvestmentDailyRateApiResource {
private final PlatformSecurityContext context;
private final InvestmentDailyRateReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List daily script rates, newest first")
public List<InvestmentDailyRateData> retrieveAll(@QueryParam("scriptId") @Parameter(description = "scriptId") final Long scriptId) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentDailyRateApiConstants.ENTITY);
return this.readPlatformService.retrieveAll(scriptId);
}
@GET
@Path("latest")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Newest rate of a script on or before a date", description = "onDate (dd-MM-yyyy) defaults to the business "
+ "date. Returns no content when no rate has been entered for the script.")
public InvestmentDailyRateData retrieveLatest(@QueryParam("scriptId") @Parameter(description = "scriptId") final Long scriptId,
@QueryParam("onDate") @Parameter(description = "dd-MM-yyyy") final String onDate) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentDailyRateApiConstants.ENTITY);
final LocalDate on = InvestmentApiDates.parse(onDate, DateUtils.getBusinessLocalDate(), "date");
return this.readPlatformService.retrieveLatest(scriptId, on);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save a daily script rate")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentDailyRate().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@DELETE
@Path("{rateId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Drop a daily script rate")
public CommandProcessingResult delete(@PathParam("rateId") @Parameter(description = "rateId") final Long rateId) {
final CommandWrapper request = new CommandWrapperBuilder().deleteInvestmentDailyRate(rateId).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,72 @@
/**
* 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.investment.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.investment.data.InvestmentDealSlipData;
import org.apache.fineract.portfolio.investment.service.InvestmentDealSlipService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/deal-slips")
@Component
@Tag(name = "Investment Deal Slips", description = "Serially numbered deal slips of every purchase, sale, refund, redemption and "
+ "switch, cancelled ones included, with the confirmation and who entered, approved and settled each deal.")
@RequiredArgsConstructor
public class InvestmentDealSlipApiResource {
/** Default window when no dates are given: the last 30 days. */
private static final int DEFAULT_DAYS = 30;
private final PlatformSecurityContext context;
private final InvestmentDealSlipService dealSlipService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Deal slip register", description = "Deals dated fromDate to toDate (dd-MM-yyyy; default the last 30 days) "
+ "in slip number order; or the one slip numbered slipNo (a purchase deal number or a sale number).")
public List<InvestmentDealSlipData> retrieve(@QueryParam("fromDate") @Parameter(description = "dd-MM-yyyy") final String fromDate,
@QueryParam("toDate") @Parameter(description = "dd-MM-yyyy") final String toDate,
@QueryParam("slipNo") @Parameter(description = "202627000001 or 202627S00001") final String slipNo) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
if (StringUtils.isNotBlank(slipNo)) {
return this.dealSlipService.slip(slipNo);
}
final LocalDate today = DateUtils.getBusinessLocalDate();
final LocalDate to = InvestmentApiDates.parse(toDate, today, "To date");
final LocalDate from = InvestmentApiDates.parse(fromDate, to.minusDays(DEFAULT_DAYS), "From date");
if (to.isBefore(from)) {
throw new GeneralPlatformDomainRuleException("error.msg.investment.dealslip.dates", "The To date cannot be before the From date");
}
return this.dealSlipService.register(from, to);
}
}
@@ -0,0 +1,78 @@
/**
* 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.investment.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.investment.data.InvestmentDiaryData;
import org.apache.fineract.portfolio.investment.service.InvestmentDiaryService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/diary")
@Component
@Tag(name = "Investment Due Date Diary", description = "Coupons, provisions, amortisation, FD payouts and maturities due in a period.")
@RequiredArgsConstructor
public class InvestmentDiaryApiResource {
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
/** Default window when no dates are given: today and the next 30 days. */
private static final int DEFAULT_DAYS = 30;
private final PlatformSecurityContext context;
private final InvestmentDiaryService diaryService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Due Date Diary for all investments", description = "fromDate / toDate dd-MM-yyyy; default today to 30 days ahead")
public InvestmentDiaryData diary(@QueryParam("fromDate") @Parameter(description = "dd-MM-yyyy") final String fromDate,
@QueryParam("toDate") @Parameter(description = "dd-MM-yyyy") final String toDate) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
final LocalDate today = DateUtils.getBusinessLocalDate();
final LocalDate from = date(fromDate, today);
final LocalDate to = date(toDate, from.plusDays(DEFAULT_DAYS));
if (to.isBefore(from)) {
throw new GeneralPlatformDomainRuleException("error.msg.investment.diary.dates", "The To date cannot be before the From date");
}
return this.diaryService.diary(from, to);
}
private static LocalDate date(final String value, final LocalDate fallback) {
if (value == null || value.isBlank()) {
return fallback;
}
try {
return LocalDate.parse(value.trim(), DATE);
} catch (final DateTimeParseException e) {
throw new GeneralPlatformDomainRuleException("error.msg.investment.diary.date.invalid", "Enter the date as DD-MM-YYYY", value);
}
}
}
@@ -0,0 +1,120 @@
/**
* 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.investment.api;
import java.math.BigDecimal;
import java.util.Set;
/** Fixed deposits placed with other banks. */
public final class InvestmentFdApiConstants {
private InvestmentFdApiConstants() {}
public static final String ENTITY = "INVESTMENTFD";
public static final String BOND_TYPE_FD = "FD";
public static final String STATUS_ACTIVE = "ACTIVE";
public static final String STATUS_MATURED = "MATURED";
public static final String STATUS_PREMATURE = "PREMATURE";
public static final String STATUS_RENEWED = "RENEWED";
/** When the interest is paid: all at maturity (the usual case), or every half year / year. */
public static final String PAYOUT_AT_MATURITY = "MATURITY";
public static final String PAYOUT_HALF_YEARLY = "HALF_YEARLY";
public static final String PAYOUT_YEARLY = "YEARLY";
public static final Set<String> PAYOUTS = Set.of(PAYOUT_AT_MATURITY, PAYOUT_HALF_YEARLY, PAYOUT_YEARLY);
/** How the maturity amount is worked out, to compare with the bank's figure (Jitendra Sir). */
public static final String CALC_SIMPLE = "SIMPLE";
public static final String CALC_MONTHLY = "MONTHLY";
public static final String CALC_QUARTERLY = "QUARTERLY";
public static final String CALC_HALF_YEARLY = "HALF_YEARLY";
public static final String CALC_YEARLY = "YEARLY";
public static final Set<String> CALC_TYPES = Set.of(CALC_SIMPLE, CALC_MONTHLY, CALC_QUARTERLY, CALC_HALF_YEARLY, CALC_YEARLY);
/** RBI: deposits with other banks within 20% of total deposits as on the previous 31 March, and within 5% with one bank. */
public static final BigDecimal GROSS_LIMIT_PERCENT = BigDecimal.valueOf(20);
public static final BigDecimal BANK_LIMIT_PERCENT = BigDecimal.valueOf(5);
/** Current accounts with banks also count towards the 20% inter-bank exposure. */
public static final String BANK_BALANCE_HEADER_GL_CODE = "100200";
/** Deposits of the bank, for the suggested total-deposits figure: everything under the deposit header, plus RD. */
public static final String DEPOSIT_HEADER_GL_CODE = "200100";
public static final String RD_DEPOSIT_GL_CODE = "200201";
public static final String PARTY_ID = "partyId";
public static final String CERT_NO = "certNo";
public static final String CERT_DATE = "certDate";
public static final String DEAL_DATE = "dealDate";
public static final String AMOUNT = "amount";
public static final String INT_RATE = "intRate";
public static final String MATURITY_DATE = "maturityDate";
public static final String PAYOUT = "payout";
public static final String CALC_TYPE = "calcType";
public static final String BANK_GL_ACCOUNT_ID = "bankGlAccountId";
public static final String REMARKS = "remarks";
public static final String AS_ON_DATE = "asOnDate";
public static final String TOTAL_DEPOSITS = "totalDeposits";
public static final String OWNED_FUNDS = "ownedFunds";
public static final String LOCALE = "locale";
public static final String DATE_FORMAT = "dateFormat";
public static final String CLOSURE_MATURITY = "MATURITY";
public static final String CLOSURE_PREMATURE = "PREMATURE";
public static final String CLOSURE_RENEWAL = "RENEWAL";
public static final String LINE_PROVISION = "PROVISION";
public static final String LINE_PAYOUT = "PAYOUT";
public static final String INTEREST_RECEIVABLE_GL_CODE = "100603";
/** Tax the other bank deducts from the interest, recoverable from the tax department. */
public static final String TDS_RECEIVABLE_GL_CODE = "100602";
public static final String CLOSE_DATE = "closeDate";
public static final String RECEIVED_AMOUNT = "receivedAmount";
public static final String TDS = "tds";
public static final String PENAL_AMOUNT = "penalAmount";
public static final String PRINCIPAL_BANK_GL_ACCOUNT_ID = "principalBankGlAccountId";
public static final String INTEREST_BANK_GL_ACCOUNT_ID = "interestBankGlAccountId";
public static final String NEW_CERT_NO = "newCertNo";
public static final String NEW_CERT_DATE = "newCertDate";
public static final String NEW_INT_RATE = "newIntRate";
public static final String NEW_MATURITY_DATE = "newMaturityDate";
public static final String NEW_PAYOUT = "newPayout";
public static final String NEW_CALC_TYPE = "newCalcType";
public static final String MATURITY_AMOUNT = "maturityAmount";
public static final String RENEWAL_AMOUNT = "renewalAmount";
public static final String DIFF_BANK_GL_ACCOUNT_ID = "diffBankGlAccountId";
public static final String LINE_MATURITY = "MATURITY";
public static final String LINE_TYPE = "lineType";
public static final String INT_DATE = "intDate";
/** Reminders show lines due within this many days (as for Govt securities). */
public static final int DUE_WITHIN_DAYS = 2;
public static final Set<String> INTEREST_PARAMS = Set.of(LINE_TYPE, INT_DATE, RECEIVED_AMOUNT, TDS, BANK_GL_ACCOUNT_ID, LOCALE,
DATE_FORMAT);
public static final Set<String> MATURE_PARAMS = Set.of(RECEIVED_AMOUNT, TDS, PRINCIPAL_BANK_GL_ACCOUNT_ID, INTEREST_BANK_GL_ACCOUNT_ID,
REMARKS, LOCALE, DATE_FORMAT);
public static final Set<String> PREMATURE_PARAMS = Set.of(CLOSE_DATE, RECEIVED_AMOUNT, TDS, PENAL_AMOUNT, PRINCIPAL_BANK_GL_ACCOUNT_ID,
INTEREST_BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
public static final Set<String> RENEW_PARAMS = Set.of(NEW_CERT_NO, NEW_CERT_DATE, NEW_INT_RATE, NEW_MATURITY_DATE, NEW_PAYOUT,
NEW_CALC_TYPE, MATURITY_AMOUNT, RENEWAL_AMOUNT, TDS, DIFF_BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
public static final Set<String> FD_PARAMS = Set.of(PARTY_ID, CERT_NO, CERT_DATE, DEAL_DATE, AMOUNT, INT_RATE, MATURITY_DATE, PAYOUT,
CALC_TYPE, BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
public static final Set<String> BASE_PARAMS = Set.of(AS_ON_DATE, TOTAL_DEPOSITS, OWNED_FUNDS, REMARKS, LOCALE, DATE_FORMAT);
}
@@ -0,0 +1,199 @@
/**
* 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.investment.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
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.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.investment.data.InvestmentFdClosureData;
import org.apache.fineract.portfolio.investment.data.InvestmentFdData;
import org.apache.fineract.portfolio.investment.data.InvestmentFdDueData;
import org.apache.fineract.portfolio.investment.data.InvestmentFdScheduleData;
import org.apache.fineract.portfolio.investment.service.InvestmentFdClosureService;
import org.apache.fineract.portfolio.investment.service.InvestmentFdInterestService;
import org.apache.fineract.portfolio.investment.service.InvestmentFdService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/fds")
@Component
@Tag(name = "Investment FD", description = "Fixed deposits the bank places with other banks.")
@RequiredArgsConstructor
public class InvestmentFdApiResource {
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private final PlatformSecurityContext context;
private final InvestmentFdService fdService;
private final InvestmentFdClosureService closureService;
private final InvestmentFdInterestService interestService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "All FDs placed with other banks")
public List<InvestmentFdData> retrieveAll() {
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
return this.fdService.retrieveAll();
}
@GET
@Path("quote")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Preview an FD: interest, maturity amount and the RBI limits", description = "Dates dd-MM-yyyy")
public InvestmentFdData quote(@QueryParam("partyId") final Long partyId, @QueryParam("amount") final BigDecimal amount,
@QueryParam("intRate") final BigDecimal intRate, @QueryParam("dealDate") final String dealDate,
@QueryParam("maturityDate") final String maturityDate, @QueryParam("payout") final String payout,
@QueryParam("calcType") final String calcType) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
return this.fdService.quote(partyId, amount, intRate, date(dealDate), date(maturityDate), payout, calcType);
}
@GET
@Path("due")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "FD interest vouchers and maturities due", description = "Due now or within withinDays (default 2)")
public List<InvestmentFdDueData> due(@QueryParam("withinDays") final Integer withinDays) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
return this.interestService.due(withinDays == null ? InvestmentFdApiConstants.DUE_WITHIN_DAYS : withinDays);
}
@GET
@Path("{fdId}/interest-schedule")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Interest lines of an FD", description = "31 Mar / 30 Sep provisions, payouts and maturity")
public InvestmentFdScheduleData schedule(@PathParam("fdId") final Long fdId) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
return this.interestService.schedule(fdId);
}
@POST
@Path("{fdId}/interest")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Post the next interest line of an FD", description = "lineType PROVISION | PAYOUT, intDate; payout: receivedAmount, tds")
public CommandProcessingResult postInterest(@PathParam("fdId") final Long fdId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
return this.commandsSourceWritePlatformService
.logCommandSource(new CommandWrapperBuilder().postInvestmentFdInterest(fdId).withJson(apiRequestBodyAsJson).build());
}
@GET
@Path("{fdId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "One FD")
public InvestmentFdData retrieveOne(@PathParam("fdId") @Parameter(description = "fdId") final Long fdId) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
return this.fdService.retrieveOne(fdId);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Place an FD", description = "Dr the bank's FD GL / Cr our bank GL, dated the deal date; refused beyond the RBI limits.")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentFd().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@GET
@Path("{fdId}/closure-quote")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Preview a maturity, premature closure or renewal", description = "type MATURITY | PREMATURE | RENEWAL")
public InvestmentFdClosureData closureQuote(@PathParam("fdId") final Long fdId, @QueryParam("type") final String type,
@QueryParam("closeDate") final String closeDate, @QueryParam("receivedAmount") final BigDecimal receivedAmount,
@QueryParam("tds") final BigDecimal tds, @QueryParam("renewalAmount") final BigDecimal renewalAmount,
@QueryParam("principalBankGlAccountId") final Long principalBankGlAccountId,
@QueryParam("interestBankGlAccountId") final Long interestBankGlAccountId,
@QueryParam("diffBankGlAccountId") final Long diffBankGlAccountId) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
return this.closureService.quote(fdId, type, date(closeDate), receivedAmount, tds, renewalAmount, principalBankGlAccountId,
interestBankGlAccountId, diffBankGlAccountId);
}
@POST
@Path("{fdId}/mature")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Close an FD at maturity", description = "Dated the maturity date; amount as credited by the bank, optional TDS.")
public CommandProcessingResult mature(@PathParam("fdId") final Long fdId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
return this.commandsSourceWritePlatformService
.logCommandSource(new CommandWrapperBuilder().matureInvestmentFd(fdId).withJson(apiRequestBodyAsJson).build());
}
@POST
@Path("{fdId}/premature")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Close an FD before maturity", description = "Not calculated: the amount received (and any penal interest) is entered.")
public CommandProcessingResult premature(@PathParam("fdId") final Long fdId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
return this.commandsSourceWritePlatformService
.logCommandSource(new CommandWrapperBuilder().prematureInvestmentFd(fdId).withJson(apiRequestBodyAsJson).build());
}
@POST
@Path("{fdId}/renew")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Renew an FD at maturity", description = "New certificate, rate and maturity; money added or taken out goes through a bank GL.")
public CommandProcessingResult renew(@PathParam("fdId") final Long fdId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
return this.commandsSourceWritePlatformService
.logCommandSource(new CommandWrapperBuilder().renewInvestmentFd(fdId).withJson(apiRequestBodyAsJson).build());
}
@POST
@Path("deposit-base")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Record total deposits as on a 31 March", description = "The base of the RBI limits for the following year.")
public CommandProcessingResult setDepositBase(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().setInvestmentDepositBase().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
private static LocalDate date(final String value) {
if (StringUtils.isBlank(value)) {
return null;
}
try {
return LocalDate.parse(value.trim(), DATE);
} catch (final java.time.format.DateTimeParseException e) {
// A half-typed date is a bad request, not a server error.
throw new GeneralPlatformDomainRuleException("error.msg.investmentfd.date.invalid", "Enter the date as DD-MM-YYYY", value);
}
}
}
@@ -0,0 +1,51 @@
/**
* 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.investment.api;
import java.util.Set;
/** Constants for posting an interest schedule line (coupon or closing provision) of a holding to GL. */
public final class InvestmentInterestApiConstants {
private InvestmentInterestApiConstants() {}
public static final String ENTITY = "INVESTMENTINTEREST";
public static final String RESOURCE = "investmentinterest";
public static final String LINE_TYPE = "lineType";
public static final String INT_DATE = "intDate";
/** Bank GL the coupon is received into; defaults to the purchase's paying bank. */
public static final String BANK_GL_ACCOUNT_ID = "bankGlAccountId";
public static final String LOCALE = "locale";
public static final String DATE_FORMAT = "dateFormat";
public static final Set<String> PARAMS = Set.of(LINE_TYPE, INT_DATE, BANK_GL_ACCOUNT_ID, LOCALE, DATE_FORMAT);
/** A line shows as a task this many days before its date (Jitendra Sir). */
public static final int DUE_WITHIN_DAYS = 2;
public static final String INTEREST_RECEIVABLE_GL_CODE = "100603";
public static final String INTEREST_INCOME_GOVT_SECURITIES_GL_CODE = "400401";
public static final String INTEREST_INCOME_BONDS_GL_CODE = "400402";
/** Income GL for a holding's interest: SLR govt securities, or bonds / debentures for the rest. */
public static String interestIncomeGlCode(final String bondType) {
return "GOVT_SECURITY".equals(bondType) ? INTEREST_INCOME_GOVT_SECURITIES_GL_CODE : INTEREST_INCOME_BONDS_GL_CODE;
}
}
@@ -0,0 +1,65 @@
/**
* 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.investment.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.investment.data.InvestmentInterestDueData;
import org.apache.fineract.portfolio.investment.data.InvestmentInterestPostingData;
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/interest-postings")
@Component
@Tag(name = "Investment Interest Posting", description = "Interest lines of holdings that are due to be posted with a voucher.")
@RequiredArgsConstructor
public class InvestmentInterestApiResource {
private final PlatformSecurityContext context;
private final InvestmentPurchaseReadPlatformService readPlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Posted interest lines", description = "Coupons and closing provisions already posted to GL with their "
+ "voucher, newest first.")
public List<InvestmentInterestPostingData> retrieveAll() {
this.context.authenticatedUser().validateHasReadPermission(InvestmentInterestApiConstants.ENTITY);
return this.readPlatformService.retrieveInterestPostings();
}
@GET
@Path("due")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Interest lines due", description = "For every active holding, its next unposted interest line when that "
+ "line's date is past or within withinDays days (default 2).")
public List<InvestmentInterestDueData> due(@QueryParam("withinDays") @Parameter(description = "withinDays") final Integer withinDays) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentInterestApiConstants.ENTITY);
return this.readPlatformService
.retrieveDueInterest(withinDays == null ? InvestmentInterestApiConstants.DUE_WITHIN_DAYS : withinDays);
}
}
@@ -0,0 +1,84 @@
/**
* 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.investment.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.time.LocalDate;
import java.time.Month;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.investment.data.InvestmentInterestReportData;
import org.apache.fineract.portfolio.investment.service.InvestmentInterestReportService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/interest-report")
@Component
@Tag(name = "Investment Interest Received & Receivable", description = "Interest received in a period and receivable at its end.")
@RequiredArgsConstructor
public class InvestmentInterestReportApiResource {
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-uuuu").withResolverStyle(ResolverStyle.STRICT);
private final PlatformSecurityContext context;
private final InvestmentInterestReportService reportService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Total Interest Received & Receivable", description = "fromDate / toDate dd-MM-yyyy; default 1 April of the "
+ "current financial year to today")
public InvestmentInterestReportData report(@QueryParam("fromDate") @Parameter(description = "dd-MM-yyyy") final String fromDate,
@QueryParam("toDate") @Parameter(description = "dd-MM-yyyy") final String toDate) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
final LocalDate today = DateUtils.getBusinessLocalDate();
final LocalDate to = date(toDate, today);
if (to.isAfter(today)) {
throw new GeneralPlatformDomainRuleException("error.msg.investment.report.future", "The To date cannot be after today");
}
// Without a From date the report runs from 1 April of the To date's financial year.
final LocalDate fyStart = LocalDate.of(to.getMonthValue() >= 4 ? to.getYear() : to.getYear() - 1, Month.APRIL, 1);
final LocalDate from = date(fromDate, fyStart);
if (to.isBefore(from)) {
throw new GeneralPlatformDomainRuleException("error.msg.investment.report.dates", "The To date cannot be before the From date");
}
return this.reportService.report(from, to);
}
private static LocalDate date(final String value, final LocalDate fallback) {
if (value == null || value.isBlank()) {
return fallback;
}
try {
return LocalDate.parse(value.trim(), DATE);
} catch (final DateTimeParseException e) {
throw new GeneralPlatformDomainRuleException("error.msg.investment.report.date.invalid", "Enter the date as DD-MM-YYYY", value);
}
}
}
@@ -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.portfolio.investment.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
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.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.investment.data.InvestmentPartyData;
import org.apache.fineract.portfolio.investment.service.InvestmentMasterReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/parties")
@Component
@Tag(name = "Investment Bank / Party Master", description = "Counterparties an investment is bought from or placed with.")
@RequiredArgsConstructor
public class InvestmentPartyApiResource {
private final PlatformSecurityContext context;
private final InvestmentMasterReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List investment parties")
public List<InvestmentPartyData> retrieveAll() {
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_PARTY);
return this.readPlatformService.retrieveAllParties();
}
@GET
@Path("{partyId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve an investment party")
public InvestmentPartyData retrieveOne(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_PARTY);
return this.readPlatformService.retrieveParty(partyId);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create an investment party")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentParty().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@PUT
@Path("{partyId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update an investment party")
public CommandProcessingResult update(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().updateInvestmentParty(partyId).withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@DELETE
@Path("{partyId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Drop an investment party")
public CommandProcessingResult delete(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId) {
final CommandWrapper request = new CommandWrapperBuilder().deleteInvestmentParty(partyId).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -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.portfolio.investment.api;
import java.util.Set;
/** Constants for the Investment Purchase Entry (a deal that creates a holding). */
public final class InvestmentPurchaseApiConstants {
private InvestmentPurchaseApiConstants() {}
public static final String ENTITY = "INVESTMENTPURCHASE";
public static final String RESOURCE = "investmentpurchase";
public static final Set<String> SETTLEMENT_TYPES = Set.of("T+0", "T+1", "T+2");
public static final Set<String> CATEGORIES = Set.of("HTM (Held To Maturity)", "AFS (Available For Sale)", "HFT (Held For Trading)");
/** Saved deal; nothing posted to GL until it is settled. */
public static final String STATUS_PENDING_SETTLEMENT = "PENDING_SETTLEMENT";
/** Settled: posted to GL on the settlement date, now a holding. */
public static final String STATUS_ACTIVE = "ACTIVE";
/** Every unit sold before maturity. */
public static final String STATUS_SOLD = "SOLD";
/** Repaid at maturity. */
public static final String STATUS_REDEEMED = "REDEEMED";
/** Dropped before settlement; kept so its deal and certificate numbers are never given out again. */
public static final String STATUS_DROPPED = "DROPPED";
/** GLs the settlement entry debits besides the script's investment GL. */
public static final String BROKEN_PERIOD_INTEREST_GL_CODE = "500606";
public static final String STAMP_DUTY_GL_CODE = "500601";
public static final String BROKERAGE_GL_CODE = "500602";
public static final String OTHER_CHARGES_GL_CODE = "500603";
public static final String BOND_TYPE = "bondType";
public static final String SCRIPT_ID = "scriptId";
public static final String PARTY_ID = "partyId";
public static final String DEAL_DATE = "dealDate";
public static final String DEAL_TIME = "dealTime";
public static final String SETTLEMENT_TYPE = "settlementType";
public static final String FACE_VALUE = "faceValue";
public static final String PURCHASE_RATE = "purchaseRate";
public static final String CONSIDER_AMT = "considerAmt";
public static final String NAV = "nav";
public static final String STAMP_DUTY = "stampDuty";
public static final String BROKERAGE = "brokerage";
public static final String OTHER_CHARGES = "otherCharges";
public static final String CATEGORY = "category";
public static final String LAST_INT_DATE = "lastIntDate";
public static final String CR_GL_ACCOUNT_ID = "crGlAccountId";
public static final String SUB_TYPE = "subType";
/** Optional legacy fields (trade no, authorities, lien, delivery mode, …) kept as a JSON object. */
public static final String OTHER_DETAILS = "otherDetails";
public static final String LOCALE = "locale";
public static final String DATE_FORMAT = "dateFormat";
public static final Set<String> PARAMS = Set.of(BOND_TYPE, SCRIPT_ID, PARTY_ID, DEAL_DATE, DEAL_TIME, SETTLEMENT_TYPE, FACE_VALUE,
PURCHASE_RATE, CONSIDER_AMT, NAV, STAMP_DUTY, BROKERAGE, OTHER_CHARGES, CATEGORY, LAST_INT_DATE, CR_GL_ACCOUNT_ID, SUB_TYPE,
OTHER_DETAILS, InvestmentApiConstants.CONFIRMATION_MODE, LOCALE, DATE_FORMAT);
/** Settle may carry the counterparty's confirmation of the deal. */
public static final Set<String> SETTLE_PARAMS = Set.of(InvestmentApiConstants.CONFIRMATION_RECEIVED_ON,
InvestmentApiConstants.CONFIRMATION_REF, LOCALE, DATE_FORMAT);
/** Dividend received on a share holding: Dr Bank / Cr 400404 Dividend on Investments, dated the day it is received. */
public static final String DIVIDEND_GL_CODE = "400404";
public static final String DIVIDEND_DATE = "dividendDate";
public static final String AMOUNT = "amount";
public static final String BANK_GL_ACCOUNT_ID = "bankGlAccountId";
public static final String REMARKS = "remarks";
public static final Set<String> DIVIDEND_PARAMS = Set.of(DIVIDEND_DATE, AMOUNT, BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
}
@@ -0,0 +1,252 @@
/**
* 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.investment.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
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.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.investment.data.InvestmentDividendData;
import org.apache.fineract.portfolio.investment.data.InvestmentInterestScheduleData;
import org.apache.fineract.portfolio.investment.data.InvestmentNonSlrLimitData;
import org.apache.fineract.portfolio.investment.data.InvestmentPurchaseData;
import org.apache.fineract.portfolio.investment.data.InvestmentSaleData;
import org.apache.fineract.portfolio.investment.data.InvestmentShareLimitData;
import org.apache.fineract.portfolio.investment.service.InvestmentNonSlrService;
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
import org.apache.fineract.portfolio.investment.service.InvestmentSaleReadPlatformService;
import org.apache.fineract.portfolio.investment.service.InvestmentShareService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/purchases")
@Component
@Tag(name = "Investment Purchase Entry", description = "Investment deals (Govt Security, Bond, Mutual Fund, …) — each becomes a holding.")
@RequiredArgsConstructor
public class InvestmentPurchaseApiResource {
private final PlatformSecurityContext context;
private final InvestmentPurchaseReadPlatformService readPlatformService;
private final InvestmentSaleReadPlatformService saleReadPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final InvestmentNonSlrService nonSlrService;
private final InvestmentShareService shareService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List investment purchases (holdings)")
public List<InvestmentPurchaseData> retrieveAll() {
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
return this.readPlatformService.retrieveAll();
}
@GET
@Path("non-slr-limit")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "RBI non-SLR limits for a purchase", description = "Where a non-SLR purchase of amount (book value) in scriptId "
+ "on dealDate (dd-MM-yyyy, default today) stands against the 10% limits; any of them may be left out.")
public InvestmentNonSlrLimitData nonSlrLimit(@QueryParam("scriptId") @Parameter(description = "scriptId") final Long scriptId,
@QueryParam("dealDate") @Parameter(description = "dd-MM-yyyy") final String dealDate,
@QueryParam("amount") @Parameter(description = "amount") final BigDecimal amount) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
final LocalDate date = InvestmentApiDates.parse(dealDate, DateUtils.getBusinessLocalDate(), "deal date");
return this.nonSlrService.limits(date, scriptId, amount);
}
@GET
@Path("share-limit")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "RBI limits for a share purchase", description = "Where a purchase of amount (book value) of shares of partyId "
+ "on dealDate (dd-MM-yyyy, default today) stands against the 2% of owned funds and 5% of the society's capital limits; "
+ "membership shares are exempt.")
public InvestmentShareLimitData shareLimit(@QueryParam("partyId") @Parameter(description = "partyId") final Long partyId,
@QueryParam("dealDate") @Parameter(description = "dd-MM-yyyy") final String dealDate,
@QueryParam("amount") @Parameter(description = "amount") final BigDecimal amount) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
final LocalDate date = InvestmentApiDates.parse(dealDate, DateUtils.getBusinessLocalDate(), "deal date");
return this.shareService.limits(date, partyId, amount);
}
@GET
@Path("dividends")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Dividends received on share holdings", description = "Newest first, with their vouchers.")
public List<InvestmentDividendData> retrieveDividends() {
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
return this.shareService.retrieveDividends();
}
@GET
@Path("{purchaseId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve an investment purchase")
public InvestmentPurchaseData retrieveOne(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
return this.readPlatformService.retrieveOne(purchaseId);
}
@GET
@Path("{purchaseId}/interest-schedule")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Interest schedule of a holding", description = "Half-yearly coupons and 31 Mar / 30 Sep closing provisions "
+ "from purchase to maturity. Calculated only — nothing is posted. asOnDate (dd-MM-yyyy) defaults to the business date.")
public InvestmentInterestScheduleData retrieveInterestSchedule(
@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
@QueryParam("asOnDate") @Parameter(description = "dd-MM-yyyy") final String asOnDate) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
final LocalDate asOn = StringUtils.isBlank(asOnDate) ? null : InvestmentApiDates.parse(asOnDate, null, "as-on date");
return this.readPlatformService.retrieveInterestSchedule(purchaseId, asOn);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save a purchase entry")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentPurchase().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@POST
@Path("{purchaseId}/settle")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Settle a purchase", description = "On or after the settlement date: posts the purchase to GL dated the "
+ "settlement date and makes the deal an active holding.")
public CommandProcessingResult settle(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().settleInvestmentPurchase(purchaseId)
.withJson(StringUtils.isBlank(apiRequestBodyAsJson) ? "{}" : apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@POST
@Path("{purchaseId}/interest-postings")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Post an interest line", description = "Posts the next interest schedule line (coupon or closing provision) "
+ "of a holding to GL with a JV voucher, on or after its date and in date order.")
public CommandProcessingResult postInterest(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().postInvestmentInterest(purchaseId).withJson(apiRequestBodyAsJson)
.build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@GET
@Path("{purchaseId}/redemption-quote")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "What the redemption at maturity will post", description = "Face value back against the book value still "
+ "held; refused before maturity or while a sale is pending.")
public InvestmentSaleData redemptionQuote(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentSaleApiConstants.ENTITY);
return this.saleReadPlatformService.quoteRedemption(purchaseId);
}
@POST
@Path("{purchaseId}/redeem")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Redeem at maturity", description = "On or after maturity, once every coupon is posted: posts the face value "
+ "received, dated the maturity date, and closes the holding.")
public CommandProcessingResult redeem(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().redeemInvestmentPurchase(purchaseId)
.withJson(StringUtils.isBlank(apiRequestBodyAsJson) ? "{}" : apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@POST
@Path("{purchaseId}/write-off")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Write off part of the book value", description = "Dr Loss on Investments / Cr Investment for the amount "
+ "given; the units stay held at the reduced book value.")
public CommandProcessingResult writeOff(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().writeOffInvestmentPurchase(purchaseId).withJson(apiRequestBodyAsJson)
.build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@POST
@Path("{purchaseId}/dividends")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Record a dividend received on a share holding", description = "dividendDate, amount, bankGlAccountId, "
+ "remarks: Dr the bank GL / Cr 400404 Dividend on Investments, dated the day it was received.")
public CommandProcessingResult postDividend(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().dividendInvestmentPurchase(purchaseId).withJson(apiRequestBodyAsJson)
.build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@POST
@Path("{purchaseId}/transfer")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "GOI Category Transfer", description = "Moves the holding to another security (scriptId) of the same coupon "
+ "rate, maturity and unit value; the book value moves between GLs only when the two scripts use different GLs.")
public CommandProcessingResult transfer(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().transferInvestmentSecurity(purchaseId).withJson(apiRequestBodyAsJson)
.build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@POST
@Path("{purchaseId}/switch")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Mutual fund switch", description = "switchDate, units, fromNav, toScriptId, toNav, subType, remarks: the units "
+ "leave this fund at fromNav and a new deal opens in the other scheme the same day; no bank leg.")
public CommandProcessingResult switchFund(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().switchInvestmentFund(purchaseId).withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@DELETE
@Path("{purchaseId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Drop a purchase entry")
public CommandProcessingResult delete(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId) {
final CommandWrapper request = new CommandWrapperBuilder().deleteInvestmentPurchase(purchaseId).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,89 @@
/**
* 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.investment.api;
import java.util.Set;
/** Sale (before maturity) and redemption (at maturity) of investment holdings. */
public final class InvestmentSaleApiConstants {
private InvestmentSaleApiConstants() {}
public static final String ENTITY = "INVESTMENTSALE";
public static final String RESOURCE = "investmentsale";
public static final String TYPE_SELL = "SELL";
public static final String TYPE_REDEEM = "REDEEM";
/** Part of the book value written off as a loss; the units stay held. */
public static final String TYPE_WRITEOFF = "WRITEOFF";
/** A year-end amortisation of premium (31 March); recorded like a sale so the book value comes down. */
public static final String TYPE_AMORTISE = "AMORTISE";
/** Units of a mutual fund switched at NAV into another scheme the same day; no money comes to the bank. */
public static final String TYPE_SWITCH = "SWITCH";
public static final String STATUS_PENDING_SETTLEMENT = "PENDING_SETTLEMENT";
public static final String STATUS_SETTLED = "SETTLED";
/** Dropped before settlement; kept so its sale number stays accounted for in the deal slip register. */
public static final String STATUS_DROPPED = "DROPPED";
/** Sales that are deals with a deal slip of their own (a sale number); write-offs and amortisation are not. */
public static final Set<String> NUMBERED_TYPES = Set.of(TYPE_SELL, TYPE_REDEEM, TYPE_SWITCH);
/** Profit on sale / redemption (income) and loss on sale / redemption (expense). */
public static final String PROFIT_ON_SALE_GL_CODE = "400405";
public static final String LOSS_ON_SALE_GL_CODE = "500605";
/** Premium still on the books at maturity is written off to the amortisation expense. */
public static final String PREMIUM_AMORTISATION_GL_CODE = "500604";
/** Premium paid over face value, held here until amortised (Jitendra Sir). */
public static final String PREMIUM_ON_INVESTMENTS_GL_CODE = "100707";
public static final String PURCHASE_ID = "purchaseId";
public static final String PARTY_ID = "partyId";
public static final String DEAL_DATE = "dealDate";
public static final String DEAL_TIME = "dealTime";
public static final String SETTLEMENT_TYPE = "settlementType";
public static final String UNITS = "units";
public static final String SALE_RATE = "saleRate";
public static final String BROKERAGE = "brokerage";
public static final String BANK_GL_ACCOUNT_ID = "bankGlAccountId";
public static final String SANCTION_AUTHORITY = "sanctionAuthority";
public static final String REMARKS = "remarks";
public static final String BROKER_CODE = "brokerCode";
public static final String AMOUNT = "amount";
public static final String FY_END = "fyEnd";
public static final String SWITCH_DATE = "switchDate";
public static final String FROM_NAV = "fromNav";
public static final String TO_SCRIPT_ID = "toScriptId";
public static final String TO_NAV = "toNav";
public static final String SUB_TYPE = "subType";
public static final String LOCALE = "locale";
public static final String DATE_FORMAT = "dateFormat";
public static final Set<String> SELL_PARAMS = Set.of(PURCHASE_ID, PARTY_ID, DEAL_DATE, DEAL_TIME, SETTLEMENT_TYPE, UNITS, SALE_RATE,
BROKERAGE, BANK_GL_ACCOUNT_ID, SANCTION_AUTHORITY, REMARKS, BROKER_CODE, InvestmentApiConstants.CONFIRMATION_MODE, LOCALE,
DATE_FORMAT);
/** Settle may carry the counterparty's confirmation of the deal. */
public static final Set<String> SETTLE_PARAMS = Set.of(InvestmentApiConstants.CONFIRMATION_RECEIVED_ON,
InvestmentApiConstants.CONFIRMATION_REF, LOCALE, DATE_FORMAT);
public static final Set<String> REDEEM_PARAMS = Set.of(BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
public static final Set<String> WRITEOFF_PARAMS = Set.of(AMOUNT, REMARKS, LOCALE, DATE_FORMAT);
public static final Set<String> AMORTISE_PARAMS = Set.of(PURCHASE_ID, FY_END, LOCALE, DATE_FORMAT);
public static final Set<String> SWITCH_PARAMS = Set.of(SWITCH_DATE, UNITS, FROM_NAV, TO_SCRIPT_ID, TO_NAV, SUB_TYPE, REMARKS, LOCALE,
DATE_FORMAT);
}
@@ -0,0 +1,113 @@
/**
* 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.investment.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
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.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.investment.data.InvestmentSaleData;
import org.apache.fineract.portfolio.investment.service.InvestmentSaleReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/sales")
@Component
@Tag(name = "Investment Sale", description = "Sale of holdings before maturity (all or part of the units), settled on the settlement date.")
@RequiredArgsConstructor
public class InvestmentSaleApiResource {
private final PlatformSecurityContext context;
private final InvestmentSaleReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List sales and redemptions, newest first")
public List<InvestmentSaleData> retrieveAll() {
this.context.authenticatedUser().validateHasReadPermission(InvestmentSaleApiConstants.ENTITY);
return this.readPlatformService.retrieveAll();
}
@GET
@Path("quote")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Work out a sale before saving it", description = "Price, interest from the buyer, cost, profit or loss "
+ "and the net amount for the units, rate and deal date given (dd-MM-yyyy).")
public InvestmentSaleData quote(@QueryParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
@QueryParam("dealDate") @Parameter(description = "dd-MM-yyyy") final String dealDate,
@QueryParam("settlementType") @Parameter(description = "T+0 / T+1 / T+2") final String settlementType,
@QueryParam("units") @Parameter(description = "units") final BigDecimal units,
@QueryParam("saleRate") @Parameter(description = "saleRate") final BigDecimal saleRate,
@QueryParam("brokerage") @Parameter(description = "brokerage") final BigDecimal brokerage,
@QueryParam("bankGlAccountId") @Parameter(description = "bankGlAccountId") final Long bankGlAccountId) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentSaleApiConstants.ENTITY);
final LocalDate deal = InvestmentApiDates.parse(dealDate, null, "deal date");
return this.readPlatformService.quoteSale(purchaseId, deal, StringUtils.defaultIfBlank(settlementType, "T+0"), units, saleRate,
brokerage, bankGlAccountId);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save a sale", description = "Saved pending; nothing reaches the GL until it is settled.")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentSale().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@POST
@Path("{saleId}/settle")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Settle a sale", description = "On or after the settlement date: posts the sale to GL dated the settlement "
+ "date and reduces the units held.")
public CommandProcessingResult settle(@PathParam("saleId") @Parameter(description = "saleId") final Long saleId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().settleInvestmentSale(saleId)
.withJson(StringUtils.isBlank(apiRequestBodyAsJson) ? "{}" : apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@DELETE
@Path("{saleId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Drop a pending sale")
public CommandProcessingResult delete(@PathParam("saleId") @Parameter(description = "saleId") final Long saleId) {
final CommandWrapper request = new CommandWrapperBuilder().deleteInvestmentSale(saleId).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,89 @@
/**
* 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.investment.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
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.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.investment.data.InvestmentScriptData;
import org.apache.fineract.portfolio.investment.service.InvestmentMasterReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/scripts")
@Component
@Tag(name = "Investment Script Master", description = "Securities / instruments the bank invests in (Govt Security, Bond, Mutual Fund, …).")
@RequiredArgsConstructor
public class InvestmentScriptApiResource {
private final PlatformSecurityContext context;
private final InvestmentMasterReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List investment scripts")
public List<InvestmentScriptData> retrieveAll() {
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_SCRIPT);
return this.readPlatformService.retrieveAllScripts();
}
@GET
@Path("{scriptId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve an investment script")
public InvestmentScriptData retrieveOne(@PathParam("scriptId") @Parameter(description = "scriptId") final Long scriptId) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_SCRIPT);
return this.readPlatformService.retrieveScript(scriptId);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create an investment script")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentScript().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@PUT
@Path("{scriptId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update an investment script")
public CommandProcessingResult update(@PathParam("scriptId") @Parameter(description = "scriptId") final Long scriptId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().updateInvestmentScript(scriptId).withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,50 @@
/**
* 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.investment.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.investment.data.InvestmentSecurityTransferData;
import org.apache.fineract.portfolio.investment.service.InvestmentSecurityTransferService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/transfers")
@Component
@Tag(name = "Investment Security Transfer", description = "GOI Category Transfer: a holding moved to another security of the same terms.")
@RequiredArgsConstructor
public class InvestmentTransferApiResource {
private final PlatformSecurityContext context;
private final InvestmentSecurityTransferService transferService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List security transfers, newest first")
public List<InvestmentSecurityTransferData> retrieveAll() {
this.context.authenticatedUser().validateHasReadPermission("INVESTMENTTRANSFER");
return this.transferService.retrieveAll();
}
}
@@ -0,0 +1,51 @@
/**
* 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.investment.api;
import java.util.Set;
/** Market valuation (MTM) of AFS and HFT holdings, with net depreciation held in the Investment Depreciation Reserve. */
public final class InvestmentValuationApiConstants {
private InvestmentValuationApiConstants() {}
public static final String ENTITY = "INVESTMENTVALUATION";
public static final String RESOURCE = "investmentvaluation";
/** Categories that are marked to market: HFT monthly, AFS quarterly (or more often). HTM is carried at cost. */
public static final Set<String> CATEGORIES = Set.of("AFS", "HFT");
/** Classes within which gains and losses are netted; one class never offsets another. */
public static final String CLASS_GOVT = "GOVT_SECURITY";
public static final String CLASS_BOND = "CORPORATE_BOND";
public static final String CLASS_FUND = "MUTUAL_FUND";
/** Shares are valued by rule, not at a market rate: face value, nil, or ₹1 per institution (see the valuation service). */
public static final String CLASS_SHARE = "SHARE";
/** Depreciation charged to P&L (Provisions &amp; Contingencies) and the reserve it is held in. */
public static final String DEPRECIATION_PROVISION_GL_CODE = "500504";
public static final String DEPRECIATION_RESERVE_GL_CODE = "200205";
public static final String VALUATION_DATE = "valuationDate";
public static final String CATEGORY = "category";
public static final String LOCALE = "locale";
public static final String DATE_FORMAT = "dateFormat";
public static final Set<String> RUN_PARAMS = Set.of(VALUATION_DATE, CATEGORY, LOCALE, DATE_FORMAT);
}
@@ -0,0 +1,92 @@
/**
* 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.investment.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
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.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.investment.data.InvestmentValuationData;
import org.apache.fineract.portfolio.investment.service.InvestmentValuationService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/valuations")
@Component
@Tag(name = "Investment Valuation", description = "Market valuation (MTM) of AFS / HFT holdings; net depreciation held in the IDR.")
@RequiredArgsConstructor
public class InvestmentValuationApiResource {
private final PlatformSecurityContext context;
private final InvestmentValuationService valuationService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Valuation runs, newest first")
public List<InvestmentValuationData> retrieveAll() {
this.context.authenticatedUser().validateHasReadPermission(InvestmentValuationApiConstants.ENTITY);
return this.valuationService.retrieveAll();
}
@GET
@Path("preview")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "What a valuation would find and post, without saving", description = "valuationDate dd-MM-yyyy (default "
+ "today), category AFS or HFT")
public InvestmentValuationData preview(@QueryParam("valuationDate") @Parameter(description = "dd-MM-yyyy") final String valuationDate,
@QueryParam("category") @Parameter(description = "AFS or HFT") final String category) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentValuationApiConstants.ENTITY);
return this.valuationService.preview(InvestmentApiDates.parse(valuationDate, DateUtils.getBusinessLocalDate(), "valuation date"),
category);
}
@GET
@Path("{valuationId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "One valuation run with its security-wise lines")
public InvestmentValuationData retrieveOne(@PathParam("valuationId") @Parameter(description = "valuationId") final Long valuationId) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentValuationApiConstants.ENTITY);
return this.valuationService.retrieveOne(valuationId);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Run and post a valuation", description = "valuationDate, category (AFS / HFT). Posts the change in IDR: "
+ "Dr 500504 / Cr 200205 when it grows, the reverse when it falls.")
public CommandProcessingResult run(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentValuation().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,64 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
/** Premium amortisation schedule of an HTM holding: one line per 31 March to maturity, then the rest at maturity. */
@Getter
@Builder
public class InvestmentAmortisationData {
private final Long purchaseId;
private final String dealNo;
private final String certNo;
private final String scriptCode;
private final String scriptDesc;
private final String category;
private final String settlementDate;
private final String maturityDate;
private final BigDecimal faceValue;
private final BigDecimal premium;
private final BigDecimal unamortisedPremium;
private final BigDecimal bookValue;
private final String premiumGlCode;
private final List<Row> rows;
@Getter
@Builder
public static class Row {
/** YEAR (31 March) or MATURITY (the remainder, written off on redemption). */
private final String kind;
private final String fyEnd;
private final String fromDate;
private final String toDate;
private final long days;
private final BigDecimal amount;
private final BigDecimal premiumAfter;
private final boolean posted;
private final String voucherNo;
private final String glTransactionId;
/** Next line to post and its 31 March has come. */
private final boolean postable;
}
}
@@ -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.investment.data;
import java.math.BigDecimal;
import lombok.Builder;
import lombok.Getter;
/** A holding's premium amortisation due for a 31 March (Amortize Yearly Process). */
@Getter
@Builder
public class InvestmentAmortisationDueData {
private final Long purchaseId;
private final String dealNo;
private final String certNo;
private final String scriptCode;
private final String scriptDesc;
private final String fyEnd;
private final String fromDate;
private final long days;
private final BigDecimal unamortisedPremium;
private final BigDecimal amount;
}
@@ -0,0 +1,80 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
/**
* Bucket Wise Investment: Govt Securities, bonds and FDs held on a date, split by the time left to maturity (up to 1
* year, over 1 to 3, over 3 to 5, over 5 years).
*/
@Getter
@Builder
public class InvestmentBucketReportData {
private final String asOnDate;
private final List<Bucket> buckets;
private final Bucket total;
private final List<Row> rows;
@Getter
@Builder
public static class Bucket {
private final String code;
private final String label;
private final int govtCount;
private final BigDecimal govtFaceValue;
private final BigDecimal govtBookValue;
private final int bondCount;
private final BigDecimal bondFaceValue;
private final BigDecimal bondBookValue;
private final int fdCount;
private final BigDecimal fdPrincipal;
/** Book value of securities and bonds plus FD principal. */
private final BigDecimal totalAmount;
}
@Getter
@Builder
public static class Row {
/** Code of the bucket the holding falls in. */
private final String bucket;
/** GOVT_SECURITY, BOND (Non-SLR and other bonds) or FD. */
private final String instrument;
private final Long refId;
private final String dealNo;
/** Script, or bank and certificate of an FD. */
private final String particulars;
/** HTM / AFS / HFT of a security; blank for an FD. */
private final String category;
private final BigDecimal rate;
private final String maturityDate;
/** Days from the as-on date to maturity; negative when maturity has passed but it is not yet redeemed or closed. */
private final long daysToMaturity;
/** Face value held, or the FD principal. */
private final BigDecimal faceValue;
/** Cost less what sales, amortisation and write-offs took off, or the FD principal. */
private final BigDecimal bookValue;
}
}
@@ -0,0 +1,50 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import java.time.format.DateTimeFormatter;
import lombok.Builder;
import lombok.Getter;
import org.apache.fineract.portfolio.investment.domain.InvestmentDailyRate;
@Getter
@Builder
public class InvestmentDailyRateData {
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private final Long id;
private final Long scriptId;
private final String scriptCode;
private final String scriptDesc;
private final String bondType;
private final String rateDate;
private final BigDecimal openRate;
private final BigDecimal highRate;
private final BigDecimal lowRate;
private final BigDecimal closeRate;
public static InvestmentDailyRateData from(final InvestmentDailyRate r) {
return InvestmentDailyRateData.builder().id(r.getId()).scriptId(r.getScript().getId()).scriptCode(r.getScript().getScriptCode())
.scriptDesc(r.getScript().getScriptDesc()).bondType(r.getScript().getBondType())
.rateDate(r.getRateDate().format(DATE_FORMAT)).openRate(r.getOpenRate()).highRate(r.getHighRate()).lowRate(r.getLowRate())
.closeRate(r.getCloseRate()).build();
}
}
@@ -0,0 +1,94 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import lombok.Builder;
import lombok.Getter;
/**
* One deal slip: a purchase (BUY) or a sale, refund, redemption or switch, with everything the slip must show — the
* counterparty, the security, amount, price, deal date and time, settlement date, how the deal was confirmed, direct or
* through a broker (and the brokerage), and who entered, approved, settled or cancelled it. A dropped deal stays on the
* register as CANCELLED so its number is accounted for.
*/
@Getter
@Builder
public class InvestmentDealSlipData {
/** Purchase deal number (202627000001) or sale number (202627S00001). */
private final String slipNo;
/** BUY, SELL, REFUND (shares), REDEEM or SWITCH. */
private final String side;
/** PENDING_SETTLEMENT, SETTLED or CANCELLED. */
private final String status;
/** Status of the purchase / sale record itself (ACTIVE, SOLD, DROPPED, …). */
private final String recordStatus;
private final Long purchaseId;
private final Long saleId;
/** Deal number of the holding (for a sale, the purchase it sells from). */
private final String dealNo;
private final String certNo;
private final String dealDate;
private final String dealTime;
private final String settlementType;
private final String settlementDate;
private final String partyCode;
private final String counterparty;
private final String bondType;
private final String scriptCode;
private final String scriptDesc;
private final String isin;
private final String category;
private final BigDecimal units;
private final BigDecimal faceValue;
/** Price per ₹100 of face value, NAV per unit or price per share. */
private final BigDecimal price;
/** Price of the units without interest or charges. */
private final BigDecimal consideration;
private final BigDecimal accruedInterest;
private final BigDecimal brokerage;
/** Stamp duty and other charges of a purchase. */
private final BigDecimal otherCharges;
/** Money paid out (purchase) or received (sale). */
private final BigDecimal netAmount;
private final String bankGlCode;
private final String bankGlName;
/** DIRECT or BROKER. */
private final String dealMode;
private final String brokerCode;
private final String tradeNo;
private final String orderNo;
private final String confirmationMode;
private final String confirmationReceivedOn;
private final String confirmationRef;
private final String enteredBy;
private final String enteredOn;
private final String approvedBy;
private final String approvedOn;
private final String settledBy;
private final String settledOn;
private final String settleApprovedBy;
private final String settleApprovedOn;
private final String cancelledBy;
private final String cancelledOn;
private final String voucherNo;
private final String glTransactionId;
private final String remarks;
}
@@ -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.investment.data;
import java.math.BigDecimal;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
/**
* Due Date Diary: every dated event of the active investments in a period — Govt Security / bond coupons and 31 Mar /
* 30 Sep provisions, maturities, premium amortisation, and FD provisions, payouts and maturities.
*/
@Getter
@Builder
public class InvestmentDiaryData {
private final String fromDate;
private final String toDate;
private final String asOnDate;
private final int pendingCount;
private final int overdueCount;
/** Money expected in the period from coupons, FD payouts and maturities not yet received. */
private final BigDecimal expectedInflow;
private final List<Row> rows;
@Getter
@Builder
public static class Row {
private final String date;
/** COUPON, PROVISION, MATURITY, AMORTISATION (Govt Security / bond); FD_PROVISION, FD_PAYOUT, FD_MATURITY. */
private final String kind;
/** SECURITY or FD — which screen the entry belongs to. */
private final String instrument;
private final Long refId;
private final String dealNo;
/** Script, or bank and certificate of an FD. */
private final String particulars;
private final BigDecimal amount;
/** POSTED, OVERDUE, DUE (today), UPCOMING. */
private final String status;
private final String voucherNo;
}
}
@@ -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.investment.data;
import java.math.BigDecimal;
import java.time.format.DateTimeFormatter;
import lombok.Builder;
import lombok.Getter;
import org.apache.fineract.portfolio.investment.domain.InvestmentDividend;
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
/** A dividend received on a share holding, with its voucher. */
@Getter
@Builder
public class InvestmentDividendData {
private final Long id;
private final Long purchaseId;
private final String dealNo;
private final String certNo;
private final String scriptCode;
private final String scriptDesc;
private final Long partyId;
private final String partyName;
private final String dividendDate;
private final BigDecimal amount;
private final String bankGlCode;
private final String bankGlName;
private final String remarks;
private final String glTransactionId;
private final String voucherNo;
public static InvestmentDividendData from(final InvestmentDividend d) {
final InvestmentPurchase p = d.getPurchase();
return InvestmentDividendData.builder().id(d.getId()).purchaseId(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo())
.scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc()).partyId(p.getParty().getId())
.partyName(p.getParty().getPartyName()).dividendDate(d.getDividendDate().format(DateTimeFormatter.ofPattern("dd-MM-yyyy")))
.amount(d.getAmount()).bankGlCode(d.getBankGlAccount().getGlCode()).bankGlName(d.getBankGlAccount().getName())
.remarks(d.getRemarks()).glTransactionId(d.getGlTransactionId()).voucherNo(d.getVoucherNo()).build();
}
}
@@ -0,0 +1,65 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
/** What closing an FD (maturity, premature or renewal) works out to, and the voucher it posts. */
@Getter
@Builder
public class InvestmentFdClosureData {
private final Long fdId;
private final String dealNo;
private final String type;
private final String closeDate;
private final BigDecimal principal;
/** Interest the system works out for the period, to compare with the bank's figure. */
private final BigDecimal expectedInterest;
/** Interest already booked by provisions and not yet received. */
private final BigDecimal provisioned;
/** Money credited to our bank by the other bank (net of TDS). */
private final BigDecimal receivedAmount;
private final BigDecimal tds;
private final BigDecimal penalAmount;
/** Gross interest as per the bank: received + TDS − principal. */
private final BigDecimal interest;
/** Interest income of this entry: interest less what was provisioned (negative = reversed from income). */
private final BigDecimal income;
/** Bank's interest less the system's. */
private final BigDecimal difference;
private final BigDecimal renewalAmount;
/** Renewal: money added (positive) or taken out (negative) against the maturity amount. */
private final BigDecimal topUp;
private final List<Line> lines;
@Getter
@AllArgsConstructor
public static class Line {
private final String side;
private final String glCode;
private final String glName;
private final BigDecimal amount;
}
}
@@ -0,0 +1,107 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import lombok.Builder;
import lombok.Getter;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.portfolio.investment.domain.InvestmentFd;
/** An FD placed with another bank; also the shape of a purchase preview (with the limit check). */
@Getter
@Builder
public class InvestmentFdData {
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private final Long id;
private final String dealNo;
private final Long partyId;
private final String partyCode;
private final String partyName;
/** TDS rate the bank deducts on interest; null when it does not deduct TDS. */
private final BigDecimal tdsRate;
private final String certNo;
private final String certDate;
private final String dealDate;
private final BigDecimal principal;
private final BigDecimal intRate;
private final String maturityDate;
private final long days;
private final String payout;
private final String calcType;
private final BigDecimal expectedInterest;
private final BigDecimal expectedMaturityAmount;
/** Interest paid each half year / year for a payout FD. */
private final BigDecimal periodInterest;
private final String fdGlCode;
private final String fdGlName;
private final String interestGlCode;
private final String interestGlName;
private final Long bankGlAccountId;
private final String bankGlCode;
private final String bankGlName;
private final String status;
private final String remarks;
private final String glTransactionId;
private final String voucherNo;
private final String renewedFromDealNo;
private final String closedOnDate;
private final String closureType;
private final BigDecimal closureAmount;
private final BigDecimal closureInterest;
private final BigDecimal closureTds;
private final BigDecimal closurePenal;
private final String closureVoucherNo;
private final String closureGlTransactionId;
private final InvestmentFdLimitData limit;
public static InvestmentFdData from(final InvestmentFd f, final BigDecimal periodInterest) {
return builder().id(f.getId()).dealNo(f.getDealNo()).partyId(f.getParty().getId()).partyCode(f.getParty().getPartyCode())
.partyName(f.getParty().getPartyName())
.tdsRate(f.getParty().isTdsApplicable() ? f.getParty().getTdsRate() : null).certNo(f.getCertNo())
.certDate(format(f.getCertDate()))
.dealDate(format(f.getDealDate())).principal(f.getPrincipal()).intRate(f.getIntRate())
.maturityDate(format(f.getMaturityDate())).days(ChronoUnit.DAYS.between(f.getDealDate(), f.getMaturityDate()))
.payout(f.getPayout()).calcType(f.getCalcType()).expectedInterest(f.getExpectedInterest())
.expectedMaturityAmount(f.getExpectedMaturityAmount()).periodInterest(periodInterest).fdGlCode(code(f.getFdGlAccount()))
.fdGlName(name(f.getFdGlAccount())).interestGlCode(code(f.getInterestGlAccount())).interestGlName(name(f.getInterestGlAccount()))
.bankGlAccountId(f.getBankGlAccount().getId()).bankGlCode(code(f.getBankGlAccount())).bankGlName(name(f.getBankGlAccount()))
.status(f.getStatus()).remarks(f.getRemarks()).glTransactionId(f.getGlTransactionId()).voucherNo(f.getVoucherNo())
.renewedFromDealNo(f.getRenewedFrom() == null ? null : f.getRenewedFrom().getDealNo()).closedOnDate(format(f.getClosedOnDate()))
.closureType(f.getClosureType()).closureAmount(f.getClosureAmount()).closureInterest(f.getClosureInterest())
.closureTds(f.getClosureTds()).closurePenal(f.getClosurePenal()).closureVoucherNo(f.getClosureVoucherNo()).closureGlTransactionId(f.getClosureGlTransactionId()).build();
}
private static String code(final GLAccount gl) {
return gl == null ? null : gl.getGlCode();
}
private static String name(final GLAccount gl) {
return gl == null ? null : gl.getName();
}
private static String format(final LocalDate date) {
return date == null ? null : date.format(DATE);
}
}
@@ -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.investment.data;
import java.math.BigDecimal;
import lombok.Builder;
import lombok.Getter;
/** The next interest voucher (or the maturity) of an FD that is due, or due within the reminder window. */
@Getter
@Builder
public class InvestmentFdDueData {
private final Long fdId;
private final String dealNo;
private final String partyName;
private final String certNo;
/** PROVISION, PAYOUT or MATURITY. */
private final String lineType;
private final String intDate;
/** What the bank credits (net of TDS); a provision line: what it books. */
private final BigDecimal amount;
private final BigDecimal grossInterest;
private final BigDecimal tds;
private final boolean postable;
private final boolean overdue;
}
@@ -0,0 +1,48 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import lombok.Builder;
import lombok.Getter;
/** Where a new FD stands against the RBI limits on deposits with other banks. */
@Getter
@Builder
public class InvestmentFdLimitData {
/** The 31 March the limits are based on. */
private final String asOnDate;
/** Total deposits as on that date, as entered; empty until someone enters it. */
private final BigDecimal totalDeposits;
/** Owned funds (paid-up share capital + reserves) as on that date, the base of the 2% limit on co-operative shares. */
private final BigDecimal ownedFunds;
/** The same figure worked out from the deposit GLs, as a suggestion. */
private final BigDecimal glTotalDeposits;
private final BigDecimal grossLimit;
private final BigDecimal fdOutstanding;
private final BigDecimal bankBalances;
private final BigDecimal grossAfter;
private final BigDecimal bankLimit;
private final String partyName;
private final BigDecimal bankAfter;
private final BigDecimal newAmount;
private final boolean withinLimits;
private final String message;
}
@@ -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.investment.data;
import java.math.BigDecimal;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
/** Interest lines of an FD: 31 Mar / 30 Sep provisions, payouts of a half-yearly / yearly FD, and maturity. */
@Getter
@Builder
public class InvestmentFdScheduleData {
private final InvestmentFdData fd;
/** Provisioned interest not yet received (interest receivable of this FD). */
private final BigDecimal provisioned;
private final List<Row> rows;
@Getter
@Builder
public static class Row {
/** PROVISION, PAYOUT or MATURITY (shown for information; closed on the Maturity screen). */
private final String lineType;
private final String intDate;
private final String fromDate;
private final String toDate;
private final long days;
private final BigDecimal grossInterest;
/** Payout: the part already provisioned, cleared from interest receivable. */
private final BigDecimal alreadyBooked;
private final BigDecimal tds;
/** Provision: interest booked. Payout: interest income of the payout (gross less already booked). */
private final BigDecimal amount;
private final boolean posted;
private final String voucherNo;
private final String glTransactionId;
/** The next line to post, and its date has come. */
private final boolean postable;
}
}
@@ -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.investment.data;
import java.math.BigDecimal;
import lombok.Builder;
import lombok.Getter;
/** The next unposted interest line of a holding that is due, or due within the reminder window — a task to post. */
@Getter
@Builder
public class InvestmentInterestDueData {
private final Long purchaseId;
private final String dealNo;
private final String certNo;
private final String scriptCode;
private final String scriptDesc;
private final String lineType;
private final String intDate;
private final BigDecimal grossInterest;
private final BigDecimal amount;
/** The date has come: it can be posted now. */
private final boolean postable;
/** The date has passed without posting. */
private final boolean overdue;
}
@@ -0,0 +1,51 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import lombok.Builder;
import lombok.Getter;
/** An interest line (coupon or closing provision) already posted to GL with its voucher — for the transaction log. */
@Getter
@Builder
public class InvestmentInterestPostingData {
private final Long id;
private final Long purchaseId;
private final String dealNo;
private final String certNo;
private final String bondType;
private final String scriptDesc;
private final String lineType;
private final String intDate;
private final String fromDate;
private final String toDate;
private final BigDecimal grossInterest;
private final BigDecimal alreadyBooked;
private final BigDecimal amount;
/** Debit side: the bank for a coupon, interest receivable for a provision. */
private final String drGlCode;
private final String drGlName;
private final String incomeGlCode;
private final String incomeGlName;
private final String glTransactionId;
private final String voucherNo;
private final String postedOnDate;
}
@@ -0,0 +1,64 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
/**
* Total Interest Received &amp; Receivable: per Govt Security / bond deal and FD, the interest received in a period and
* the interest earned up to its last day that has not come in yet.
*/
@Getter
@Builder
public class InvestmentInterestReportData {
private final String fromDate;
private final String toDate;
private final BigDecimal totalReceived;
private final BigDecimal totalReceivable;
private final BigDecimal totalProvisioned;
private final BigDecimal total;
private final List<Row> rows;
@Getter
@Builder
public static class Row {
/** SECURITY or FD. */
private final String instrument;
private final Long refId;
private final String dealNo;
/** Script, or bank and certificate of an FD. */
private final String particulars;
private final String status;
/** Face value of the deal as bought, or the FD principal. */
private final BigDecimal principal;
private final BigDecimal rate;
/** Interest that came in during the period (coupons, FD payouts, interest paid at closure), gross of TDS. */
private final BigDecimal received;
/** Interest earned up to the To date and not yet received. */
private final BigDecimal receivable;
/** Part of the receivable already booked by 31 Mar / 30 Sep provisions (Interest Receivable 100603). */
private final BigDecimal provisioned;
private final BigDecimal total;
}
}
@@ -0,0 +1,91 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
/**
* Interest schedule of one holding: every half-yearly coupon and every 31 March / 30 September closing provision from
* purchase to maturity. Calculated, not yet posted.
*/
@Getter
@Builder
public class InvestmentInterestScheduleData {
private final Long purchaseId;
private final String dealNo;
private final String certNo;
private final String scriptCode;
private final String scriptDesc;
private final BigDecimal faceValue;
private final BigDecimal unitValue;
private final BigDecimal balanceUnits;
private final BigDecimal purchaseRate;
private final BigDecimal bookValue;
private final BigDecimal intRate;
private final String dealDate;
private final String lastIntDate;
private final String maturityDate;
/**
* Broken-period interest paid to the seller at purchase — booked as an expense (GL 500606), not in this schedule.
*/
private final BigDecimal purchasedAccruedInterest;
private final BigDecimal couponPerHalfYear;
private final String asOnDate;
private final String interestGlCode;
private final String interestGlName;
/** Bank the coupon is received into by default: the purchase's paying bank. */
private final Long bankGlAccountId;
private final String bankGlCode;
private final String bankGlName;
private final String status;
private final List<Row> rows;
@Getter
@Builder
public static class Row {
/** COUPON (cash received) or PROVISION (closing accrual). */
private final String type;
private final String intDate;
private final String fromDate;
private final String toDate;
private final int days;
/** Coupon received, or interest earned since the last coupon up to the closing. */
private final BigDecimal grossInterest;
/** Interest Receivable balance before this line (purchased accrued interest / earlier provision). */
private final BigDecimal alreadyBooked;
/** Provision to book now, or the part of the coupon that is new income. */
private final BigDecimal amount;
/** True when the date is on or before the as-on date. */
private final boolean due;
/** Posted to GL: its JV voucher and GL transaction. */
private final boolean posted;
private final String voucherNo;
private final String glTransactionId;
private final String postedOnDate;
/** The next line to post and its date has come. */
private final boolean postable;
/** Unposted and its date is within the reminder window (or past). */
private final boolean dueSoon;
}
}
@@ -0,0 +1,50 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import lombok.Builder;
import lombok.Getter;
/** Where a new non-SLR purchase stands against the RBI limits on non-SLR and unlisted non-SLR investments. */
@Getter
@Builder
public class InvestmentNonSlrLimitData {
/** The 31 March the limits are based on. */
private final String asOnDate;
/** Total deposits as on that date, as entered for the FD limits; empty until someone enters it. */
private final BigDecimal totalDeposits;
/** 10% of total deposits. */
private final BigDecimal nonSlrLimit;
/** Book value of non-SLR investments held or pending settlement, before this deal. */
private final BigDecimal nonSlrOutstanding;
private final BigDecimal nonSlrAfter;
/** Book value of non-SLR investments held on that 31 March. */
private final BigDecimal nonSlrBase;
/** 10% of the non-SLR investments held on that 31 March. */
private final BigDecimal unlistedLimit;
private final BigDecimal unlistedOutstanding;
private final BigDecimal unlistedAfter;
private final BigDecimal newAmount;
/** Whether this deal's security is listed or proposed to be listed; unlisted ones also count against the unlisted limit. */
private final boolean listed;
private final boolean withinLimits;
private final String message;
}
@@ -0,0 +1,80 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import java.time.format.DateTimeFormatter;
import lombok.Builder;
import lombok.Getter;
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
@Getter
@Builder
public class InvestmentPartyData {
private final Long id;
private final String partyCode;
private final String bondType;
private final String partyName;
private final String address1;
private final String address2;
private final String address3;
private final String city;
private final String pincode;
private final String phone;
private final String fax;
private final String bankType;
private final Long glAccountId;
private final String glCode;
private final String glName;
private final Long interestGlAccountId;
private final String interestGlCode;
private final String interestGlName;
private final boolean tdsApplicable;
private final BigDecimal tdsRate;
private final String rbiCode;
private final BigDecimal limit;
private final String leiNo;
private final String cpBankName;
private final String cpBranchName;
private final String cpAccountNo;
private final String cpIfsc;
private final String institutionType;
private final BigDecimal subscribedCapital;
private final String lastBalanceSheetDate;
private final boolean inLiquidation;
private final boolean active;
public static InvestmentPartyData from(final InvestmentParty p) {
return InvestmentPartyData.builder().id(p.getId()).partyCode(p.getPartyCode()).bondType(p.getBondType())
.partyName(p.getPartyName()).address1(p.getAddress1()).address2(p.getAddress2()).address3(p.getAddress3())
.city(p.getCity()).pincode(p.getPincode()).phone(p.getPhone()).fax(p.getFax()).bankType(p.getBankType())
.glAccountId(p.getGlAccount().getId()).glCode(p.getGlAccount().getGlCode()).glName(p.getGlAccount().getName())
.interestGlAccountId(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getId())
.interestGlCode(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getGlCode())
.interestGlName(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getName())
.tdsApplicable(p.isTdsApplicable()).tdsRate(p.getTdsRate()).rbiCode(p.getRbiCode()).limit(p.getExposureLimit())
.leiNo(p.getLeiNo()).cpBankName(p.getCpBankName())
.cpBranchName(p.getCpBranchName()).cpAccountNo(p.getCpAccountNo()).cpIfsc(p.getCpIfsc())
.institutionType(p.getInstitutionType()).subscribedCapital(p.getSubscribedCapital())
.lastBalanceSheetDate(p.getLastBalanceSheetDate() == null ? null
: p.getLastBalanceSheetDate().format(DateTimeFormatter.ofPattern("dd-MM-yyyy")))
.inLiquidation(p.isInLiquidation()).active(p.isActive()).build();
}
}
@@ -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.investment.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import lombok.Builder;
import lombok.Getter;
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
@Getter
@Builder
public class InvestmentPurchaseData {
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private final Long id;
private final String dealNo;
private final String certNo;
private final String bondType;
private final Long scriptId;
private final String scriptCode;
private final String scriptDesc;
private final String isin;
private final Long partyId;
private final String partyCode;
private final String partyName;
private final String dealDate;
private final String dealTime;
private final String settlementType;
private final String settlementDate;
private final BigDecimal faceValue;
private final BigDecimal purchaseRate;
private final BigDecimal units;
private final BigDecimal balanceUnits;
private final BigDecimal brokerage;
private final BigDecimal stampDuty;
private final BigDecimal otherCharges;
private final BigDecimal bookValue;
private final BigDecimal premiumDiscount;
private final BigDecimal intRate;
private final String issueDate;
private final String maturityDate;
private final String lastIntDate;
private final Integer accruedDays;
private final BigDecimal accruedInterest;
private final BigDecimal settlementAmount;
private final BigDecimal ytm;
private final String category;
private final String subType;
private final Long investmentGlAccountId;
private final String investmentGlCode;
private final String investmentGlName;
private final Long crGlAccountId;
private final String crGlCode;
private final String crGlName;
private final String otherDetails;
private final String confirmationMode;
private final String confirmationReceivedOn;
private final String confirmationRef;
private final String status;
private final String glTransactionId;
private final String settledOnDate;
public static InvestmentPurchaseData from(final InvestmentPurchase p) {
return InvestmentPurchaseData.builder().id(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo()).bondType(p.getBondType())
.scriptId(p.getScript().getId()).scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc())
.isin(p.getScript().getIsin()).partyId(p.getParty().getId()).partyCode(p.getParty().getPartyCode())
.partyName(p.getParty().getPartyName()).dealDate(format(p.getDealDate())).dealTime(p.getDealTime())
.settlementType(p.getSettlementType()).settlementDate(format(p.getSettlementDate())).faceValue(p.getFaceValue())
.purchaseRate(p.getPurchaseRate()).units(p.getUnits()).balanceUnits(p.getBalanceUnits()).brokerage(p.getBrokerage())
.stampDuty(p.getStampDuty()).otherCharges(p.getOtherCharges()).bookValue(p.getBookValue())
.premiumDiscount(p.getPremiumDiscount()).intRate(p.getIntRate()).issueDate(format(p.getIssueDate()))
.maturityDate(format(p.getMaturityDate())).lastIntDate(format(p.getLastIntDate())).accruedDays(p.getAccruedDays())
.accruedInterest(p.getAccruedInterest()).settlementAmount(p.getSettlementAmount()).ytm(p.getYtm()).category(p.getCategory())
.subType(p.getSubType()).investmentGlAccountId(p.getInvestmentGlAccount().getId())
.investmentGlCode(p.getInvestmentGlAccount().getGlCode()).investmentGlName(p.getInvestmentGlAccount().getName())
.crGlAccountId(p.getCrGlAccount().getId()).crGlCode(p.getCrGlAccount().getGlCode()).crGlName(p.getCrGlAccount().getName())
.otherDetails(p.getOtherDetails()).confirmationMode(p.getConfirmationMode())
.confirmationReceivedOn(format(p.getConfirmationReceivedOn())).confirmationRef(p.getConfirmationRef())
.status(p.getStatus()).glTransactionId(p.getGlTransactionId())
.settledOnDate(format(p.getSettledOnDate())).build();
}
private static String format(final LocalDate date) {
return date == null ? null : date.format(DATE_FORMAT);
}
}
@@ -0,0 +1,110 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import lombok.Builder;
import lombok.Getter;
import org.apache.fineract.portfolio.investment.domain.InvestmentSale;
/** A sale or redemption of a holding — also the shape of a quote before it is saved. */
@Getter
@Builder
public class InvestmentSaleData {
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private final Long id;
private final Long purchaseId;
private final String dealNo;
/** Deal slip number of the sale itself, e.g. 202627S00001 (empty for a write-off or amortisation). */
private final String saleNo;
private final String certNo;
private final String bondType;
private final String scriptCode;
private final String scriptDesc;
private final String saleType;
private final Long partyId;
private final String partyName;
private final String dealDate;
private final String dealTime;
private final String settlementType;
private final String settlementDate;
private final BigDecimal units;
private final BigDecimal faceValue;
private final BigDecimal saleRate;
private final BigDecimal consideration;
private final Integer accruedDays;
private final BigDecimal accruedInterest;
private final BigDecimal receivableCleared;
/** Interest from the buyer not yet booked: accrued interest less the receivable cleared. */
private final BigDecimal interestIncome;
private final BigDecimal brokerage;
private final BigDecimal bookValueSold;
private final BigDecimal premiumCleared;
private final BigDecimal profitLoss;
private final BigDecimal netAmount;
private final Long bankGlAccountId;
private final String bankGlCode;
private final String bankGlName;
private final String investmentGlCode;
/** GL the premium share is credited to: 100707, or the investment GL for deals settled before it was used. */
private final String premiumGlCode;
private final String sanctionAuthority;
private final String remarks;
private final String brokerCode;
private final String confirmationMode;
private final String confirmationReceivedOn;
private final String confirmationRef;
private final String status;
private final String glTransactionId;
private final String voucherNo;
private final String settledOnDate;
public static InvestmentSaleData from(final InvestmentSale s) {
return builder().id(s.getId()).purchaseId(s.getPurchase().getId()).dealNo(s.getPurchase().getDealNo()).saleNo(s.getSaleNo())
.certNo(s.getPurchase().getCertNo()).bondType(s.getPurchase().getBondType())
.scriptCode(s.getPurchase().getScript().getScriptCode()).scriptDesc(s.getPurchase().getScript().getScriptDesc())
.saleType(s.getSaleType()).partyId(s.getParty() == null ? null : s.getParty().getId())
.partyName(s.getParty() == null ? null : s.getParty().getPartyName()).dealDate(format(s.getDealDate()))
.dealTime(s.getDealTime()).settlementType(s.getSettlementType()).settlementDate(format(s.getSettlementDate()))
.units(s.getUnits()).faceValue(s.getFaceValue()).saleRate(s.getSaleRate()).consideration(s.getConsideration())
.accruedDays(s.getAccruedDays()).accruedInterest(s.getAccruedInterest()).receivableCleared(s.getReceivableCleared())
.interestIncome(zero(s.getAccruedInterest()).subtract(zero(s.getReceivableCleared()))).brokerage(s.getBrokerage())
.bookValueSold(s.getBookValueSold()).premiumCleared(s.getPremiumCleared()).profitLoss(s.getProfitLoss()).netAmount(s.getNetAmount())
.bankGlAccountId(s.getBankGlAccount().getId()).bankGlCode(s.getBankGlAccount().getGlCode())
.bankGlName(s.getBankGlAccount().getName()).investmentGlCode(s.getPurchase().getInvestmentGlAccount().getGlCode())
.premiumGlCode(s.getPurchase().getPremiumGlAccount() != null ? s.getPurchase().getPremiumGlAccount().getGlCode()
: s.getPurchase().getInvestmentGlAccount().getGlCode())
.sanctionAuthority(s.getSanctionAuthority()).remarks(s.getRemarks()).brokerCode(s.getBrokerCode())
.confirmationMode(s.getConfirmationMode()).confirmationReceivedOn(format(s.getConfirmationReceivedOn()))
.confirmationRef(s.getConfirmationRef()).status(s.getStatus())
.glTransactionId(s.getGlTransactionId()).voucherNo(s.getVoucherNo()).settledOnDate(format(s.getSettledOnDate())).build();
}
private static BigDecimal zero(final BigDecimal v) {
return v == null ? BigDecimal.ZERO : v;
}
private static String format(final LocalDate date) {
return date == null ? null : date.format(DATE_FORMAT);
}
}
@@ -0,0 +1,76 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import lombok.Builder;
import lombok.Getter;
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
@Getter
@Builder
public class InvestmentScriptData {
/** Dates go out as DD-MM-YYYY, the format the Investment screens use throughout. */
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private final Long id;
private final String scriptCode;
private final String bondType;
private final String slrType;
private final String scriptDesc;
private final Long glAccountId;
private final String glCode;
private final String glName;
private final String isin;
private final BigDecimal faceValuePerUnit;
private final BigDecimal intRate;
private final String issueDate;
private final String maturityDate;
private final String companyName;
private final String address1;
private final String address2;
private final String address3;
private final String city;
private final String pincode;
private final String phone;
private final String fax;
private final String corpCode;
private final String rating;
private final String ratingAgency;
private final boolean listed;
private final boolean active;
public static InvestmentScriptData from(final InvestmentScript s) {
return InvestmentScriptData.builder().id(s.getId()).scriptCode(s.getScriptCode()).bondType(s.getBondType())
.slrType(s.getSlrType()).scriptDesc(s.getScriptDesc()).glAccountId(s.getGlAccount().getId())
.glCode(s.getGlAccount().getGlCode()).glName(s.getGlAccount().getName()).isin(s.getIsin())
.faceValuePerUnit(s.getFaceValuePerUnit()).intRate(s.getIntRate()).issueDate(format(s.getIssueDate()))
.maturityDate(format(s.getMaturityDate())).companyName(s.getCompanyName()).address1(s.getAddress1())
.address2(s.getAddress2()).address3(s.getAddress3()).city(s.getCity()).pincode(s.getPincode()).phone(s.getPhone())
.fax(s.getFax()).corpCode(s.getCorpCode()).rating(s.getRating()).ratingAgency(s.getRatingAgency()).listed(s.isListed())
.active(s.isActive()).build();
}
private static String format(final LocalDate date) {
return date == null ? null : date.format(DATE_FORMAT);
}
}
@@ -0,0 +1,62 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import java.time.format.DateTimeFormatter;
import lombok.Builder;
import lombok.Getter;
import org.apache.fineract.portfolio.investment.domain.InvestmentSecurityTransfer;
/** A GOI Category Transfer of a holding, for the transaction log. */
@Getter
@Builder
public class InvestmentSecurityTransferData {
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private final Long id;
private final Long purchaseId;
private final String dealNo;
private final String certNo;
private final String bondType;
private final String fromScriptCode;
private final String fromScriptDesc;
private final String toScriptCode;
private final String toScriptDesc;
private final String fromGlCode;
private final String toGlCode;
private final String toGlName;
private final String transferDate;
private final BigDecimal units;
private final BigDecimal bookValue;
private final String remarks;
private final String glTransactionId;
private final String voucherNo;
public static InvestmentSecurityTransferData from(final InvestmentSecurityTransfer t) {
return builder().id(t.getId()).purchaseId(t.getPurchase().getId()).dealNo(t.getPurchase().getDealNo())
.certNo(t.getPurchase().getCertNo()).bondType(t.getPurchase().getBondType()).fromScriptCode(t.getFromScript().getScriptCode())
.fromScriptDesc(t.getFromScript().getScriptDesc()).toScriptCode(t.getToScript().getScriptCode())
.toScriptDesc(t.getToScript().getScriptDesc()).fromGlCode(t.getFromGlAccount().getGlCode())
.toGlCode(t.getToGlAccount().getGlCode()).toGlName(t.getToGlAccount().getName())
.transferDate(t.getTransferDate().format(DATE_FORMAT)).units(t.getUnits()).bookValue(t.getBookValue())
.remarks(t.getRemarks()).glTransactionId(t.getGlTransactionId()).voucherNo(t.getVoucherNo()).build();
}
}
@@ -0,0 +1,51 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import lombok.Builder;
import lombok.Getter;
/**
* Where a share purchase stands against the RBI limits: shares of co-operative societies within 2% of owned funds, and
* within 5% of one society's subscribed capital. Membership shares (DCCB, State Co-op Bank, NPCI / CCIL / SWIFT, Umbrella
* Organisation, IDPIC) may exceed them.
*/
@Getter
@Builder
public class InvestmentShareLimitData {
/** The 31 March the owned funds are taken from. */
private final String asOnDate;
private final String institutionType;
/** True for membership shares, which the limits do not bind. */
private final boolean exempt;
private final BigDecimal ownedFunds;
private final BigDecimal coopLimit;
private final BigDecimal coopOutstanding;
private final BigDecimal coopAfter;
private final String partyName;
private final BigDecimal subscribedCapital;
private final BigDecimal institutionLimit;
private final BigDecimal institutionOutstanding;
private final BigDecimal institutionAfter;
private final BigDecimal newAmount;
private final boolean withinLimits;
private final String message;
}
@@ -0,0 +1,88 @@
/**
* 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.investment.data;
import java.math.BigDecimal;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
/**
* A market valuation (MTM) of AFS or HFT holdings: a preview before posting (no id) or a posted run. Each security is
* valued at the Daily Script Master rate; the IDR needed per class is its net loss, a net gain is ignored.
*/
@Getter
@Builder
public class InvestmentValuationData {
private final Long id;
private final String valuationDate;
private final String category;
private final BigDecimal requiredTotal;
/** IDR held from the category's previous run. */
private final BigDecimal previousTotal;
/** Posted (or to be posted): more provision when positive, a write-back when negative. */
private final BigDecimal delta;
private final String provisionGlCode;
private final String reserveGlCode;
private final String glTransactionId;
private final String voucherNo;
/** Date of the category's last run, if any; a new run must be dated after it. */
private final String lastRunDate;
/** Scripts held with no rate on or before the valuation date: the run is refused until they are entered. */
private final List<String> missingRates;
/** A preview can be posted: no rate missing, dated after the last run, and something to value or to write back. */
private final boolean postable;
private final List<ClassTotal> classes;
private final List<Line> lines;
@Getter
@Builder
public static class ClassTotal {
/** GOVT_SECURITY, CORPORATE_BOND or MUTUAL_FUND. */
private final String assetClass;
private final BigDecimal bookValue;
private final BigDecimal marketValue;
private final BigDecimal netDifference;
private final BigDecimal required;
}
@Getter
@Builder
public static class Line {
private final Long purchaseId;
private final String dealNo;
private final String certNo;
private final String bondType;
private final String scriptCode;
private final String scriptDesc;
private final String assetClass;
private final BigDecimal units;
private final BigDecimal bookValue;
private final BigDecimal rate;
private final String rateDate;
private final BigDecimal marketValue;
private final BigDecimal difference;
/** How a share was valued; empty for a holding valued at its market rate. */
private final String valuationBasis;
private final boolean npi;
}
}
@@ -0,0 +1,62 @@
/**
* 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.investment.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 lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/**
* Daily Script Master: the market rate of a script for one day, as the bank's existing CBS keeps it. Purchase Entry
* takes its rate from here (Jitendra Sir). Close rate is mandatory; open, high and low are optional.
*/
@Entity
@Table(name = "m_investment_daily_rate")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentDailyRate extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne
@JoinColumn(name = "script_id", nullable = false)
private InvestmentScript script;
@Column(name = "rate_date", nullable = false)
private LocalDate rateDate;
@Column(name = "open_rate", scale = 6, precision = 19)
private BigDecimal openRate;
@Column(name = "high_rate", scale = 6, precision = 19)
private BigDecimal highRate;
@Column(name = "low_rate", scale = 6, precision = 19)
private BigDecimal lowRate;
@Column(name = "close_rate", scale = 6, precision = 19, nullable = false)
private BigDecimal closeRate;
}
@@ -0,0 +1,34 @@
/**
* 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.investment.domain;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface InvestmentDailyRateRepository extends JpaRepository<InvestmentDailyRate, Long> {
boolean existsByScriptIdAndRateDate(Long scriptId, LocalDate rateDate);
/** The newest rate of a script on or before a date. */
Optional<InvestmentDailyRate> findFirstByScriptIdAndRateDateLessThanEqualOrderByRateDateDesc(Long scriptId, LocalDate onDate);
List<InvestmentDailyRate> findByScriptIdOrderByRateDateDesc(Long scriptId);
}
@@ -0,0 +1,51 @@
/**
* 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.investment.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/** The bank's total deposits as on a 31 March: the base for the RBI limits on deposits placed with other banks. */
@Entity
@Table(name = "m_investment_deposit_base")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentDepositBase extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "as_on_date", nullable = false, unique = true)
private LocalDate asOnDate;
@Column(name = "total_deposits", scale = 6, precision = 19, nullable = false)
private BigDecimal totalDeposits;
/** Owned funds (paid-up share capital + reserves) on the same date: shares of co-operative societies are limited to 2% of it. */
@Column(name = "owned_funds", scale = 6, precision = 19)
private BigDecimal ownedFunds;
@Column(name = "remarks", length = 500)
private String remarks;
}
@@ -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.investment.domain;
import java.time.LocalDate;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface InvestmentDepositBaseRepository extends JpaRepository<InvestmentDepositBase, Long> {
Optional<InvestmentDepositBase> findByAsOnDate(LocalDate asOnDate);
}
@@ -0,0 +1,64 @@
/**
* 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.investment.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 lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/** A dividend received on a share holding: booked as income when it is received (Dr Bank / Cr Dividend on Investments). */
@Entity
@Table(name = "m_investment_dividend")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentDividend extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne
@JoinColumn(name = "purchase_id", nullable = false)
private InvestmentPurchase purchase;
@Column(name = "dividend_date", nullable = false)
private LocalDate dividendDate;
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
private BigDecimal amount;
@ManyToOne
@JoinColumn(name = "bank_gl_account_id", nullable = false)
private GLAccount bankGlAccount;
@Column(name = "remarks", length = 500)
private String remarks;
@Column(name = "gl_transaction_id", length = 50)
private String glTransactionId;
@Column(name = "voucher_no", length = 30)
private String voucherNo;
}
@@ -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.investment.domain;
import java.time.LocalDate;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface InvestmentDividendRepository extends JpaRepository<InvestmentDividend, Long> {
List<InvestmentDividend> findAllByOrderByDividendDateDescIdDesc();
/** Whether the institution has paid us any dividend on or before a date (on any of our holdings of its shares). */
boolean existsByPurchasePartyIdAndDividendDateLessThanEqual(Long partyId, LocalDate date);
}
@@ -0,0 +1,135 @@
/**
* 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.investment.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 lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/** A fixed deposit the bank has placed with another bank, from placement to maturity, premature closure or renewal. */
@Entity
@Table(name = "m_investment_fd")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentFd extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "deal_no", nullable = false, length = 20, unique = true)
private String dealNo;
@ManyToOne
@JoinColumn(name = "party_id", nullable = false)
private InvestmentParty party;
/** The bank's FD GL (from the party), debited when the money is placed. */
@ManyToOne
@JoinColumn(name = "fd_gl_account_id", nullable = false)
private GLAccount fdGlAccount;
@ManyToOne
@JoinColumn(name = "interest_gl_account_id", nullable = false)
private GLAccount interestGlAccount;
/** Certificate (receipt) number issued by the other bank. */
@Column(name = "cert_no", nullable = false, length = 50)
private String certNo;
@Column(name = "cert_date")
private LocalDate certDate;
@Column(name = "deal_date", nullable = false)
private LocalDate dealDate;
@Column(name = "principal", scale = 6, precision = 19, nullable = false)
private BigDecimal principal;
@Column(name = "int_rate", scale = 6, precision = 19, nullable = false)
private BigDecimal intRate;
@Column(name = "maturity_date", nullable = false)
private LocalDate maturityDate;
@Column(name = "payout", nullable = false, length = 20)
private String payout;
@Column(name = "calc_type", nullable = false, length = 20)
private String calcType;
@Column(name = "expected_interest", scale = 6, precision = 19)
private BigDecimal expectedInterest;
@Column(name = "expected_maturity_amount", scale = 6, precision = 19)
private BigDecimal expectedMaturityAmount;
/** Our bank account the money was paid from. */
@ManyToOne
@JoinColumn(name = "bank_gl_account_id", nullable = false)
private GLAccount bankGlAccount;
/** The FD this one renews, if any. */
@ManyToOne
@JoinColumn(name = "renewed_from_id")
private InvestmentFd renewedFrom;
@Column(name = "status", nullable = false, length = 20)
private String status;
@Column(name = "remarks", length = 500)
private String remarks;
@Column(name = "gl_transaction_id", length = 50)
private String glTransactionId;
@Column(name = "voucher_no", length = 30)
private String voucherNo;
@Column(name = "closed_on_date")
private LocalDate closedOnDate;
/** MATURITY, PREMATURE or RENEWAL. */
@Column(name = "closure_type", length = 20)
private String closureType;
@Column(name = "closure_amount", scale = 6, precision = 19)
private BigDecimal closureAmount;
@Column(name = "closure_interest", scale = 6, precision = 19)
private BigDecimal closureInterest;
@Column(name = "closure_tds", scale = 6, precision = 19)
private BigDecimal closureTds;
@Column(name = "closure_penal", scale = 6, precision = 19)
private BigDecimal closurePenal;
@Column(name = "closure_gl_transaction_id", length = 50)
private String closureGlTransactionId;
@Column(name = "closure_voucher_no", length = 30)
private String closureVoucherNo;
}
@@ -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.portfolio.investment.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 lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/** A posted interest line of an FD: a 31 Mar / 30 Sep provision, or a half-yearly / yearly payout. */
@Entity
@Table(name = "m_investment_fd_interest")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentFdInterest extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne
@JoinColumn(name = "fd_id", nullable = false)
private InvestmentFd fd;
/** PROVISION or PAYOUT. */
@Column(name = "line_type", nullable = false, length = 20)
private String lineType;
@Column(name = "int_date", nullable = false)
private LocalDate intDate;
@Column(name = "from_date")
private LocalDate fromDate;
@Column(name = "to_date")
private LocalDate toDate;
@Column(name = "days")
private Integer days;
@Column(name = "gross_interest", scale = 6, precision = 19)
private BigDecimal grossInterest;
/** For a payout: the part already provisioned, cleared from interest receivable. */
@Column(name = "already_booked", scale = 6, precision = 19)
private BigDecimal alreadyBooked;
@Column(name = "tds", scale = 6, precision = 19)
private BigDecimal tds;
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
private BigDecimal amount;
@ManyToOne
@JoinColumn(name = "bank_gl_account_id")
private GLAccount bankGlAccount;
@Column(name = "gl_transaction_id", length = 50)
private String glTransactionId;
@Column(name = "voucher_no", length = 30)
private String voucherNo;
@Column(name = "posted_on_date")
private LocalDate postedOnDate;
}
@@ -0,0 +1,27 @@
/**
* 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.investment.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface InvestmentFdInterestRepository extends JpaRepository<InvestmentFdInterest, Long> {
List<InvestmentFdInterest> findByFdIdOrderByIntDateAscIdAsc(Long fdId);
}
@@ -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.investment.domain;
import jakarta.persistence.LockModeType;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface InvestmentFdRepository extends JpaRepository<InvestmentFd, Long> {
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
@Query("select f from InvestmentFd f where f.id = :fdId")
Optional<InvestmentFd> findByIdWithLock(@Param("fdId") Long fdId);
List<InvestmentFd> findByStatusOrderByIdAsc(String status);
boolean existsByPartyIdAndCertNoIgnoreCase(Long partyId, String certNo);
}
@@ -0,0 +1,84 @@
/**
* 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.investment.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 lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/** One interest schedule line of a holding (coupon received or closing provision) that has been posted to GL. */
@Entity
@Table(name = "m_investment_interest_posting")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentInterestPosting extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne
@JoinColumn(name = "purchase_id", nullable = false)
private InvestmentPurchase purchase;
/** COUPON or PROVISION. */
@Column(name = "line_type", nullable = false, length = 20)
private String lineType;
@Column(name = "int_date", nullable = false)
private LocalDate intDate;
@Column(name = "from_date")
private LocalDate fromDate;
@Column(name = "to_date")
private LocalDate toDate;
@Column(name = "days")
private Integer days;
@Column(name = "gross_interest", scale = 6, precision = 19)
private BigDecimal grossInterest;
@Column(name = "already_booked", scale = 6, precision = 19)
private BigDecimal alreadyBooked;
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
private BigDecimal amount;
/** Bank the coupon was received into (coupon lines only). */
@ManyToOne
@JoinColumn(name = "bank_gl_account_id")
private GLAccount bankGlAccount;
@Column(name = "gl_transaction_id", length = 50)
private String glTransactionId;
@Column(name = "voucher_no", length = 30)
private String voucherNo;
@Column(name = "posted_on_date")
private LocalDate postedOnDate;
}
@@ -0,0 +1,27 @@
/**
* 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.investment.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface InvestmentInterestPostingRepository extends JpaRepository<InvestmentInterestPosting, Long> {
List<InvestmentInterestPosting> findByPurchaseIdOrderByIntDateAsc(Long purchaseId);
}
@@ -0,0 +1,135 @@
/**
* 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.investment.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 lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/**
* Bank / Party Master — the counterparty an investment is bought from or placed with (bank, RBI, issuer, AMC, broker),
* following the bank's existing CBS Bank / Party Master.
*/
@Entity
@Table(name = "m_investment_party")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentParty extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "party_code", nullable = false, length = 20, unique = true)
private String partyCode;
@Column(name = "bond_type", nullable = false, length = 20)
private String bondType;
@Column(name = "party_name", nullable = false, length = 150)
private String partyName;
@Column(name = "address1", length = 150)
private String address1;
@Column(name = "address2", length = 150)
private String address2;
@Column(name = "address3", length = 150)
private String address3;
@Column(name = "city", length = 100)
private String city;
@Column(name = "pincode", length = 10)
private String pincode;
@Column(name = "phone", length = 20)
private String phone;
@Column(name = "fax", length = 20)
private String fax;
@Column(name = "bank_type", length = 60)
private String bankType;
@ManyToOne
@JoinColumn(name = "gl_account_id", nullable = false)
private GLAccount glAccount;
/** Interest income GL of the FDs placed with this bank (Bond Type FD only). */
@ManyToOne
@JoinColumn(name = "interest_gl_account_id")
private GLAccount interestGlAccount;
/** Whether this bank deducts TDS from the interest on FDs placed with it (some banks do, some do not). */
@Column(name = "tds_applicable", nullable = false)
private boolean tdsApplicable;
/** TDS rate in per cent, set only when the bank deducts TDS. */
@Column(name = "tds_rate", scale = 4, precision = 9)
private BigDecimal tdsRate;
@Column(name = "rbi_code", length = 30)
private String rbiCode;
@Column(name = "exposure_limit", scale = 6, precision = 19)
private BigDecimal exposureLimit;
@Column(name = "lei_no", length = 30)
private String leiNo;
@Column(name = "cp_bank_name", length = 150)
private String cpBankName;
@Column(name = "cp_branch_name", length = 150)
private String cpBranchName;
@Column(name = "cp_account_no", length = 40)
private String cpAccountNo;
@Column(name = "cp_ifsc", length = 11)
private String cpIfsc;
/** Share institutions only: DCCB, State Co-op Bank, other co-operative society, or MIC / Umbrella Organisation. */
@Column(name = "institution_type", length = 30)
private String institutionType;
/** The institution's subscribed share capital: our holding in a co-operative society is limited to 5% of it. */
@Column(name = "subscribed_capital", scale = 6, precision = 19)
private BigDecimal subscribedCapital;
/** Date of the institution's latest balance sheet we have; one older than 18 months values its shares at ₹1. */
@Column(name = "last_balance_sheet_date")
private LocalDate lastBalanceSheetDate;
/** An institution in liquidation: its shares are fully provided for. */
@Column(name = "is_in_liquidation", nullable = false)
private boolean inLiquidation;
/** An inactive party is kept for history but not offered for new deals. */
@Column(name = "is_active", nullable = false)
private boolean active = true;
}

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