Compare commits

..
Author SHA1 Message Date
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
135 changed files with 12748 additions and 0 deletions
@@ -95,6 +95,198 @@ 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 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";
@@ -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);
}
@@ -0,0 +1,83 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.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.time.LocalDate;
import java.time.format.DateTimeFormatter;
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(LocalDate.parse(fyEnd.trim(), DateTimeFormatter.ofPattern("dd-MM-yyyy")));
}
@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,122 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.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");
/** 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";
public static final String PARTY_CODE_PREFIX = "PTY-";
// 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";
// 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, LOCALE);
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,66 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.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.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-yyyy");
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());
}
try {
return this.reportService.report(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);
}
}
}
@@ -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,96 @@
/**
* 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.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.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 = StringUtils.isBlank(onDate) ? DateUtils.getBusinessLocalDate()
: LocalDate.parse(onDate.trim(), DateTimeFormatter.ofPattern("dd-MM-yyyy"));
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,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,119 @@
/**
* 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 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, 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,79 @@
/**
* 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 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-yyyy");
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 fyStart = LocalDate.of(today.getMonthValue() >= 4 ? today.getYear() : today.getYear() - 1, Month.APRIL, 1);
final LocalDate from = date(fromDate, fyStart);
final LocalDate to = date(toDate, today);
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,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.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, LOCALE, DATE_FORMAT);
}
@@ -0,0 +1,203 @@
/**
* 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.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.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
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.service.InvestmentNonSlrService;
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
import org.apache.fineract.portfolio.investment.service.InvestmentSaleReadPlatformService;
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;
@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 = StringUtils.isBlank(dealDate) ? DateUtils.getBusinessLocalDate()
: LocalDate.parse(dealDate.trim(), DateTimeFormatter.ofPattern("dd-MM-yyyy"));
return this.nonSlrService.limits(date, scriptId, amount);
}
@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
: LocalDate.parse(asOnDate.trim(), DateTimeFormatter.ofPattern("dd-MM-yyyy"));
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}/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);
}
@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,70 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.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";
public static final String STATUS_PENDING_SETTLEMENT = "PENDING_SETTLEMENT";
public static final String STATUS_SETTLED = "SETTLED";
/** 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 AMOUNT = "amount";
public static final String FY_END = "fyEnd";
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, 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);
}
@@ -0,0 +1,116 @@
/**
* 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.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.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 static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
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 = LocalDate.parse(dealDate.trim(), 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,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,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,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,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;
/** 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;
private final BigDecimal amount;
private final boolean postable;
private final boolean overdue;
}
@@ -0,0 +1,46 @@
/**
* 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;
/** 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 held, 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,71 @@
/**
* 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;
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 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()).active(p.isActive()).build();
}
}
@@ -0,0 +1,100 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.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 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()).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,102 @@
/**
* 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;
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 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())
.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()).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,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,47 @@
/**
* 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;
@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,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,118 @@
/**
* 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 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;
/** An inactive party is kept for history but not offered for new deals. */
@Column(name = "is_active", nullable = false)
private boolean active = true;
}
@@ -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 org.springframework.data.jpa.repository.JpaRepository;
public interface InvestmentPartyRepository extends JpaRepository<InvestmentParty, Long> {
/** Locks the bank so two FDs with it are checked against the 5% limit one after the other. */
@org.springframework.data.jpa.repository.Lock(value = jakarta.persistence.LockModeType.PESSIMISTIC_WRITE)
@org.springframework.data.jpa.repository.Query("select p from InvestmentParty p where p.id = :partyId")
java.util.Optional<InvestmentParty> findByIdWithLock(@org.springframework.data.repository.query.Param("partyId") Long partyId);}
@@ -0,0 +1,163 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.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;
/**
* Investment Purchase Entry — one deal (as in the bank's existing CBS "Govt.Security/Bond Entry"). Once saved it is the
* holding that interest, sale and redemption later work against; {@link #balanceUnits} tracks what is still held. Every
* derived figure (units, book value, accrued interest, settlement amount) is computed by the backend.
*/
@Entity
@Table(name = "m_investment_purchase")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentPurchase extends AbstractAuditableWithUTCDateTimeCustom<Long> {
/** Financial-year deal number, e.g. 202627000001 for the first deal of FY 2026-27. */
@Column(name = "deal_no", nullable = false, length = 20, unique = true)
private String dealNo;
/** Certificate number — system-generated running number. */
@Column(name = "cert_no", nullable = false, length = 20, unique = true)
private String certNo;
@Column(name = "bond_type", nullable = false, length = 20)
private String bondType;
@ManyToOne
@JoinColumn(name = "script_id", nullable = false)
private InvestmentScript script;
@ManyToOne
@JoinColumn(name = "party_id", nullable = false)
private InvestmentParty party;
@Column(name = "deal_date", nullable = false)
private LocalDate dealDate;
@Column(name = "deal_time", length = 8)
private String dealTime;
@Column(name = "settlement_type", nullable = false, length = 5)
private String settlementType;
@Column(name = "settlement_date", nullable = false)
private LocalDate settlementDate;
@Column(name = "face_value", scale = 6, precision = 19, nullable = false)
private BigDecimal faceValue;
/** Price per ₹100 face value (Govt Security / Bond); NAV for a mutual fund. */
@Column(name = "purchase_rate", scale = 6, precision = 19, nullable = false)
private BigDecimal purchaseRate;
@Column(name = "units", scale = 6, precision = 19, nullable = false)
private BigDecimal units;
@Column(name = "balance_units", scale = 6, precision = 19, nullable = false)
private BigDecimal balanceUnits;
@Column(name = "brokerage", scale = 6, precision = 19)
private BigDecimal brokerage;
@Column(name = "stamp_duty", scale = 6, precision = 19)
private BigDecimal stampDuty;
@Column(name = "other_charges", scale = 6, precision = 19)
private BigDecimal otherCharges;
@Column(name = "book_value", scale = 6, precision = 19, nullable = false)
private BigDecimal bookValue;
@Column(name = "premium_discount", scale = 6, precision = 19)
private BigDecimal premiumDiscount;
@Column(name = "int_rate", scale = 4, precision = 9)
private BigDecimal intRate;
@Column(name = "issue_date")
private LocalDate issueDate;
@Column(name = "maturity_date")
private LocalDate maturityDate;
@Column(name = "last_int_date")
private LocalDate lastIntDate;
@Column(name = "accrued_days")
private Integer accruedDays;
/** Interest paid to the seller from the last coupon date up to the deal date (30/360). */
@Column(name = "accrued_interest", scale = 6, precision = 19)
private BigDecimal accruedInterest;
@Column(name = "settlement_amount", scale = 6, precision = 19, nullable = false)
private BigDecimal settlementAmount;
@Column(name = "ytm", scale = 4, precision = 9)
private BigDecimal ytm;
@Column(name = "category", nullable = false, length = 40)
private String category;
@Column(name = "sub_type", length = 40)
private String subType;
/** Investment GL the holding is booked to (from the script). */
@ManyToOne
@JoinColumn(name = "investment_gl_account_id", nullable = false)
private GLAccount investmentGlAccount;
/** Bank GL the purchase is paid from ("CR. To A/c"). */
@ManyToOne
@JoinColumn(name = "cr_gl_account_id", nullable = false)
private GLAccount crGlAccount;
/** GL holding the premium paid over face value (100707) until it is amortised; empty when it sits in the investment GL. */
@ManyToOne
@JoinColumn(name = "premium_gl_account_id")
private GLAccount premiumGlAccount;
@Column(name = "other_details", length = 4000)
private String otherDetails;
@Column(name = "status", nullable = false, length = 20)
private String status;
/** Journal entry transaction posted when the deal was settled. */
@Column(name = "gl_transaction_id", length = 50)
private String glTransactionId;
@Column(name = "settled_on_date")
private LocalDate settledOnDate;
}
@@ -0,0 +1,36 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.domain;
import jakarta.persistence.LockModeType;
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 InvestmentPurchaseRepository extends JpaRepository<InvestmentPurchase, Long> {
/** Locks the deal row so two postings on one deal (double click, two users) run one after the other. */
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
@Query("select p from InvestmentPurchase p where p.id = :purchaseId")
Optional<InvestmentPurchase> findByIdWithLock(@Param("purchaseId") Long purchaseId);
boolean existsByScriptIdAndStatusNot(Long scriptId, String status);
}
@@ -0,0 +1,132 @@
/**
* 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 sale of some or all units of a holding before maturity (SELL), or its repayment at maturity (REDEEM). */
@Entity
@Table(name = "m_investment_sale")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentSale extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne
@JoinColumn(name = "purchase_id", nullable = false)
private InvestmentPurchase purchase;
/** SELL, REDEEM, WRITEOFF or AMORTISE. */
@Column(name = "sale_type", nullable = false, length = 10)
private String saleType;
/** Buyer of a sale; empty for a redemption. */
@ManyToOne
@JoinColumn(name = "party_id")
private InvestmentParty party;
@Column(name = "deal_date", nullable = false)
private LocalDate dealDate;
@Column(name = "deal_time", length = 8)
private String dealTime;
@Column(name = "settlement_type", length = 5)
private String settlementType;
@Column(name = "settlement_date", nullable = false)
private LocalDate settlementDate;
@Column(name = "units", scale = 6, precision = 19, nullable = false)
private BigDecimal units;
@Column(name = "face_value", scale = 6, precision = 19, nullable = false)
private BigDecimal faceValue;
/** Price per ₹100 of face value; 100 for a redemption. */
@Column(name = "sale_rate", scale = 6, precision = 19)
private BigDecimal saleRate;
/** Price received for the units, without interest. */
@Column(name = "consideration", scale = 6, precision = 19, nullable = false)
private BigDecimal consideration;
@Column(name = "accrued_days")
private Integer accruedDays;
/** Interest the buyer pays us for the days since the last coupon. */
@Column(name = "accrued_interest", scale = 6, precision = 19)
private BigDecimal accruedInterest;
/** Part of that interest already booked by a closing provision, cleared from interest receivable. */
@Column(name = "receivable_cleared", scale = 6, precision = 19)
private BigDecimal receivableCleared;
@Column(name = "brokerage", scale = 6, precision = 19)
private BigDecimal brokerage;
/** Cost of the units sold, taken off the investment GL. */
@Column(name = "book_value_sold", scale = 6, precision = 19, nullable = false)
private BigDecimal bookValueSold;
/** Part of the book value taken off that was unamortised premium (credited to the premium GL). */
@Column(name = "premium_cleared", scale = 6, precision = 19)
private BigDecimal premiumCleared;
/** Consideration less cost: positive is profit, negative is loss. */
@Column(name = "profit_loss", scale = 6, precision = 19, nullable = false)
private BigDecimal profitLoss;
/** Money into the bank: consideration plus interest, less brokerage. */
@Column(name = "net_amount", scale = 6, precision = 19, nullable = false)
private BigDecimal netAmount;
@ManyToOne
@JoinColumn(name = "bank_gl_account_id", nullable = false)
private GLAccount bankGlAccount;
@Column(name = "sanction_authority", length = 100)
private String sanctionAuthority;
@Column(name = "remarks", length = 500)
private String remarks;
@Column(name = "status", nullable = false, length = 20)
private String status;
@Column(name = "gl_transaction_id", length = 50)
private String glTransactionId;
@Column(name = "voucher_no", length = 30)
private String voucherNo;
@Column(name = "settled_on_date")
private LocalDate settledOnDate;
}
@@ -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.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 InvestmentSaleRepository extends JpaRepository<InvestmentSale, Long> {
List<InvestmentSale> findByPurchaseIdOrderByIdAsc(Long purchaseId);
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
@Query("select s from InvestmentSale s where s.id = :saleId")
Optional<InvestmentSale> findByIdWithLock(@Param("saleId") Long saleId);
/** The deal of a sale, without loading either, so the deal can be locked first. */
@Query("select s.purchase.id from InvestmentSale s where s.id = :saleId")
Optional<Long> findPurchaseIdBySaleId(@Param("saleId") Long saleId);
}
@@ -0,0 +1,118 @@
/**
* 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;
/**
* Script Master — what is being bought (a Govt Security, bond, mutual fund scheme, share, …), following the bank's
* existing CBS Script Master. A purchase picks these terms up instead of the dealer re-typing them.
*/
@Entity
@Table(name = "m_investment_script")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentScript extends AbstractAuditableWithUTCDateTimeCustom<Long> {
/** System-generated: bond-type letter + running number, e.g. G00001. */
@Column(name = "script_code", nullable = false, length = 20, unique = true)
private String scriptCode;
@Column(name = "bond_type", nullable = false, length = 20)
private String bondType;
/** GOI / STATE_GOVT / TREASURY / NON_SLR. */
@Column(name = "slr_type", length = 20)
private String slrType;
@Column(name = "script_desc", nullable = false, length = 150)
private String scriptDesc;
@ManyToOne
@JoinColumn(name = "gl_account_id", nullable = false)
private GLAccount glAccount;
@Column(name = "isin", length = 20)
private String isin;
@Column(name = "face_value_per_unit", scale = 6, precision = 19, nullable = false)
private BigDecimal faceValuePerUnit;
@Column(name = "int_rate", scale = 4, precision = 9)
private BigDecimal intRate;
@Column(name = "issue_date")
private LocalDate issueDate;
@Column(name = "maturity_date")
private LocalDate maturityDate;
@Column(name = "company_name", length = 150)
private String companyName;
@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 = "corp_code", length = 30)
private String corpCode;
/** Credit rating, long-term (AAA … D) for bonds or short-term (A1+ … A4) for CPs / CDs; UNRATED when none. */
@Column(name = "rating", length = 10)
private String rating;
@Column(name = "rating_agency", length = 30)
private String ratingAgency;
/** Listed on an exchange, or proposed to be listed (RBI counts those as listed when bought). */
@Column(name = "is_listed", nullable = false)
private boolean listed = true;
@Column(name = "is_active", nullable = false)
private boolean active = true;
}
@@ -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 InvestmentScriptRepository extends JpaRepository<InvestmentScript, Long> {
List<InvestmentScript> findByIsinIgnoreCase(String isin);
}
@@ -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.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 holding moved from one government security script to another of the same terms (GOI Category Transfer). */
@Entity
@Table(name = "m_investment_security_transfer")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentSecurityTransfer extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne
@JoinColumn(name = "purchase_id", nullable = false)
private InvestmentPurchase purchase;
@ManyToOne
@JoinColumn(name = "from_script_id", nullable = false)
private InvestmentScript fromScript;
@ManyToOne
@JoinColumn(name = "to_script_id", nullable = false)
private InvestmentScript toScript;
@ManyToOne
@JoinColumn(name = "from_gl_account_id", nullable = false)
private GLAccount fromGlAccount;
@ManyToOne
@JoinColumn(name = "to_gl_account_id", nullable = false)
private GLAccount toGlAccount;
@Column(name = "transfer_date", nullable = false)
private LocalDate transferDate;
@Column(name = "units", scale = 6, precision = 19, nullable = false)
private BigDecimal units;
/** Book value carried over; moved between GLs only when the two scripts map to different GLs. */
@Column(name = "book_value", scale = 6, precision = 19, nullable = false)
private BigDecimal bookValue;
@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,23 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface InvestmentSecurityTransferRepository extends JpaRepository<InvestmentSecurityTransfer, Long> {}
@@ -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.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class InvestmentDailyRateNotFoundException extends AbstractPlatformResourceNotFoundException {
public InvestmentDailyRateNotFoundException(final Long id) {
super("error.msg.investmentdailyrate.id.invalid", "Daily script rate with identifier " + id + " does not exist", id);
}
}
@@ -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.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class InvestmentPartyNotFoundException extends AbstractPlatformResourceNotFoundException {
public InvestmentPartyNotFoundException(final Long id) {
super("error.msg.investmentparty.id.invalid", "Investment party with identifier " + id + " does not exist", id);
}
}
@@ -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.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class InvestmentPurchaseNotFoundException extends AbstractPlatformResourceNotFoundException {
public InvestmentPurchaseNotFoundException(final Long id) {
super("error.msg.investmentpurchase.id.invalid", "Investment purchase with identifier " + id + " does not exist", id);
}
}
@@ -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.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class InvestmentScriptNotFoundException extends AbstractPlatformResourceNotFoundException {
public InvestmentScriptNotFoundException(final Long id) {
super("error.msg.investmentscript.id.invalid", "Investment script with identifier " + id + " does not exist", id);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentAmortisationService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTSALE", action = "AMORTISE")
@RequiredArgsConstructor
public class AmortiseInvestmentPremiumCommandHandler implements NewCommandSourceHandler {
private final InvestmentAmortisationService amortisationService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.amortisationService.amortise(command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentDailyRateWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTDAILYRATE", action = "CREATE")
@RequiredArgsConstructor
public class CreateInvestmentDailyRateCommandHandler implements NewCommandSourceHandler {
private final InvestmentDailyRateWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createDailyRate(command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentFdService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTFD", action = "CREATE")
@RequiredArgsConstructor
public class CreateInvestmentFdCommandHandler implements NewCommandSourceHandler {
private final InvestmentFdService fdService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.fdService.create(command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentMasterWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTPARTY", action = "CREATE")
@RequiredArgsConstructor
public class CreateInvestmentPartyCommandHandler implements NewCommandSourceHandler {
private final InvestmentMasterWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createParty(command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTPURCHASE", action = "CREATE")
@RequiredArgsConstructor
public class CreateInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
private final InvestmentPurchaseWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createPurchase(command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentSaleWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTSALE", action = "CREATE")
@RequiredArgsConstructor
public class CreateInvestmentSaleCommandHandler implements NewCommandSourceHandler {
private final InvestmentSaleWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createSale(command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentMasterWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTSCRIPT", action = "CREATE")
@RequiredArgsConstructor
public class CreateInvestmentScriptCommandHandler implements NewCommandSourceHandler {
private final InvestmentMasterWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createScript(command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentDailyRateWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTDAILYRATE", action = "DELETE")
@RequiredArgsConstructor
public class DeleteInvestmentDailyRateCommandHandler implements NewCommandSourceHandler {
private final InvestmentDailyRateWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.deleteDailyRate(command.entityId());
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentMasterWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTPARTY", action = "DELETE")
@RequiredArgsConstructor
public class DeleteInvestmentPartyCommandHandler implements NewCommandSourceHandler {
private final InvestmentMasterWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.deleteParty(command.entityId());
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTPURCHASE", action = "DELETE")
@RequiredArgsConstructor
public class DeleteInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
private final InvestmentPurchaseWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.deletePurchase(command.entityId());
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentSaleWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTSALE", action = "DELETE")
@RequiredArgsConstructor
public class DeleteInvestmentSaleCommandHandler implements NewCommandSourceHandler {
private final InvestmentSaleWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.deleteSale(command.entityId());
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentFdClosureService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTFD", action = "MATURE")
@RequiredArgsConstructor
public class MatureInvestmentFdCommandHandler implements NewCommandSourceHandler {
private final InvestmentFdClosureService closureService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.closureService.mature(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentFdInterestService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTFD", action = "POSTINTEREST")
@RequiredArgsConstructor
public class PostInvestmentFdInterestCommandHandler implements NewCommandSourceHandler {
private final InvestmentFdInterestService interestService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.interestService.post(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentInterestPostingWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTINTEREST", action = "CREATE")
@RequiredArgsConstructor
public class PostInvestmentInterestCommandHandler implements NewCommandSourceHandler {
private final InvestmentInterestPostingWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.postInterest(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentFdClosureService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTFD", action = "PREMATURE")
@RequiredArgsConstructor
public class PrematureInvestmentFdCommandHandler implements NewCommandSourceHandler {
private final InvestmentFdClosureService closureService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.closureService.premature(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentSaleWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTSALE", action = "REDEEM")
@RequiredArgsConstructor
public class RedeemInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
private final InvestmentSaleWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.redeem(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentFdClosureService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTFD", action = "RENEW")
@RequiredArgsConstructor
public class RenewInvestmentFdCommandHandler implements NewCommandSourceHandler {
private final InvestmentFdClosureService closureService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.closureService.renew(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentFdService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTFD", action = "SETBASE")
@RequiredArgsConstructor
public class SetInvestmentDepositBaseCommandHandler implements NewCommandSourceHandler {
private final InvestmentFdService fdService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.fdService.setDepositBase(command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTPURCHASE", action = "SETTLE")
@RequiredArgsConstructor
public class SettleInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
private final InvestmentPurchaseWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.settlePurchase(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentSaleWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTSALE", action = "SETTLE")
@RequiredArgsConstructor
public class SettleInvestmentSaleCommandHandler implements NewCommandSourceHandler {
private final InvestmentSaleWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.settleSale(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentSecurityTransferService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTTRANSFER", action = "CREATE")
@RequiredArgsConstructor
public class TransferInvestmentSecurityCommandHandler implements NewCommandSourceHandler {
private final InvestmentSecurityTransferService transferService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.transferService.transfer(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentMasterWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTPARTY", action = "UPDATE")
@RequiredArgsConstructor
public class UpdateInvestmentPartyCommandHandler implements NewCommandSourceHandler {
private final InvestmentMasterWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.updateParty(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentMasterWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTSCRIPT", action = "UPDATE")
@RequiredArgsConstructor
public class UpdateInvestmentScriptCommandHandler implements NewCommandSourceHandler {
private final InvestmentMasterWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.updateScript(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.investment.service.InvestmentSaleWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTSALE", action = "WRITEOFF")
@RequiredArgsConstructor
public class WriteOffInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
private final InvestmentSaleWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.writeOff(command.entityId(), command);
}
}
@@ -0,0 +1,103 @@
/**
* 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.serialization;
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.CLOSE_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.HIGH_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.LOW_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.OPEN_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.RATE_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.RESOURCE;
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.SCRIPT_ID;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.stereotype.Component;
/** Validates a Daily Script Master entry: close rate is mandatory and the day's rates must sit inside low to high. */
@Component
@RequiredArgsConstructor
public final class InvestmentDailyRateCommandFromApiJsonDeserializer {
private final FromJsonHelper fromApiJsonHelper;
public void validateForCreate(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, PARAMS);
final JsonElement element = this.fromApiJsonHelper.parse(json);
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
v.reset().parameter(SCRIPT_ID).value(this.fromApiJsonHelper.extractLongNamed(SCRIPT_ID, element)).notNull().longGreaterThanZero();
v.reset().parameter(RATE_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(RATE_DATE, element)).notNull();
final BigDecimal close = amount(CLOSE_RATE, element);
final BigDecimal open = amount(OPEN_RATE, element);
final BigDecimal high = amount(HIGH_RATE, element);
final BigDecimal low = amount(LOW_RATE, element);
v.reset().parameter(CLOSE_RATE).value(close).notNull().positiveAmount();
v.reset().parameter(OPEN_RATE).value(open).ignoreIfNull().positiveAmount();
v.reset().parameter(HIGH_RATE).value(high).ignoreIfNull().positiveAmount();
v.reset().parameter(LOW_RATE).value(low).ignoreIfNull().positiveAmount();
if (high != null && low != null && high.compareTo(low) < 0) {
v.reset().parameter(HIGH_RATE).failWithCodeAndMessage("less.than.low.rate", "High Rate cannot be below Low Rate");
}
outsideRange(v, CLOSE_RATE, "Close Rate", close, low, high);
outsideRange(v, OPEN_RATE, "Open Rate", open, low, high);
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
private static void outsideRange(final DataValidatorBuilder v, final String param, final String label, final BigDecimal rate,
final BigDecimal low, final BigDecimal high) {
if (rate == null) {
return;
}
if (low != null && rate.compareTo(low) < 0) {
v.reset().parameter(param).failWithCodeAndMessage("below.low.rate", label + " cannot be below Low Rate");
} else if (high != null && rate.compareTo(high) > 0) {
v.reset().parameter(param).failWithCodeAndMessage("above.high.rate", label + " cannot be above High Rate");
}
}
private BigDecimal amount(final String param, final JsonElement element) {
return this.fromApiJsonHelper.parameterExists(param, element)
? this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(param, element)
: null;
}
}
@@ -0,0 +1,73 @@
/**
* 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.serialization;
import static org.apache.fineract.portfolio.investment.api.InvestmentInterestApiConstants.BANK_GL_ACCOUNT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentInterestApiConstants.INT_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentInterestApiConstants.LINE_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentInterestApiConstants.PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentInterestApiConstants.RESOURCE;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.investment.service.InvestmentInterestCalculator;
import org.springframework.stereotype.Component;
/** Validates a request to post one interest schedule line. */
@Component
@RequiredArgsConstructor
public final class InvestmentInterestCommandFromApiJsonDeserializer {
private final FromJsonHelper fromApiJsonHelper;
public void validateForPost(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, PARAMS);
final JsonElement element = this.fromApiJsonHelper.parse(json);
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
final String lineType = this.fromApiJsonHelper.extractStringNamed(LINE_TYPE, element);
v.reset().parameter(LINE_TYPE).value(lineType).notBlank();
if (StringUtils.isNotBlank(lineType)) {
v.reset().parameter(LINE_TYPE).value(lineType).isOneOfTheseValues(InvestmentInterestCalculator.TYPE_COUPON,
InvestmentInterestCalculator.TYPE_PROVISION);
}
v.reset().parameter(INT_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(INT_DATE, element)).notNull();
v.reset().parameter(BANK_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(BANK_GL_ACCOUNT_ID, element)).ignoreIfNull()
.longGreaterThanZero();
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
}
@@ -0,0 +1,294 @@
/**
* 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.serialization;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS1;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS2;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS3;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BANK_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BOND_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BOND_TYPES;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CITY;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.COMPANY_NAME;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CORP_CODE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_ACCOUNT_NO;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BANK_NAME;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BRANCH_NAME;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_IFSC;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.DEBT_BOND_TYPES;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.EXPOSURE_LIMIT;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FACE_VALUE_PER_UNIT;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FAX;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.GL_ACCOUNT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INTEREST_GL_ACCOUNT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INT_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISIN;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISSUE_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.LEI_NO;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.LISTED;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.MATURITY_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_NAME;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PHONE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PINCODE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RATED_BOND_TYPES;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RATING;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RATINGS;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RATING_AGENCIES;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RATING_AGENCY;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RBI_CODE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RESOURCE_PARTY;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RESOURCE_SCRIPT;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_DESC;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPES;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.TDS_RATE;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.stereotype.Component;
/** Validates Bank / Party Master and Script Master requests before anything is written. */
@Component
@RequiredArgsConstructor
public final class InvestmentMasterCommandFromApiJsonDeserializer {
private static final String IFSC_PATTERN = "^[A-Z]{4}0[A-Z0-9]{6}$";
private static final String PINCODE_PATTERN = "^[0-9]{6}$";
private final FromJsonHelper fromApiJsonHelper;
// -------------------- Bank / Party Master --------------------
public void validateForCreateParty(final String json) {
validateParty(json, true);
}
public void validateForUpdateParty(final String json) {
validateParty(json, false);
}
private void validateParty(final String json, final boolean create) {
final JsonElement element = parse(json, PARTY_PARAMS);
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE_PARTY);
validateBondType(element, v, create);
validateRequiredString(element, v, PARTY_NAME, 150, create);
validateGlAccountId(element, v, create);
if (this.fromApiJsonHelper.parameterExists(INTEREST_GL_ACCOUNT_ID, element)) {
v.reset().parameter(INTEREST_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(INTEREST_GL_ACCOUNT_ID, element))
.ignoreIfNull().longGreaterThanZero();
}
validateAddress(element, v);
validateOptionalString(element, v, BANK_TYPE, 60);
validateOptionalString(element, v, RBI_CODE, 30);
validateOptionalString(element, v, LEI_NO, 30);
validateOptionalString(element, v, CP_BANK_NAME, 150);
validateOptionalString(element, v, CP_BRANCH_NAME, 150);
validateOptionalString(element, v, CP_ACCOUNT_NO, 40);
final String ifsc = this.fromApiJsonHelper.extractStringNamed(CP_IFSC, element);
if (StringUtils.isNotBlank(ifsc)) {
v.reset().parameter(CP_IFSC).value(ifsc.trim().toUpperCase()).matchesRegularExpression(IFSC_PATTERN,
"IFSC must be 11 characters: 4 letters, 0, then 6 letters/digits");
}
if (this.fromApiJsonHelper.parameterExists(EXPOSURE_LIMIT, element)) {
final BigDecimal limit = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(EXPOSURE_LIMIT, element);
v.reset().parameter(EXPOSURE_LIMIT).value(limit).ignoreIfNull().zeroOrPositiveAmount();
}
if (this.fromApiJsonHelper.parameterExists(TDS_RATE, element)) {
final BigDecimal rate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(TDS_RATE, element);
v.reset().parameter(TDS_RATE).value(rate).ignoreIfNull().positiveAmount().notGreaterThanMax(BigDecimal.valueOf(100));
}
throwIfErrors(errors);
}
// -------------------- Script Master --------------------
public void validateForCreateScript(final String json) {
validateScript(json, true);
}
public void validateForUpdateScript(final String json, final String existingBondType) {
validateScript(json, false, existingBondType);
}
private void validateScript(final String json, final boolean create) {
validateScript(json, create, null);
}
private void validateScript(final String json, final boolean create, final String existingBondType) {
final JsonElement element = parse(json, SCRIPT_PARAMS);
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE_SCRIPT);
validateBondType(element, v, create);
validateRequiredString(element, v, SCRIPT_DESC, 150, create);
validateGlAccountId(element, v, create);
final String slrType = this.fromApiJsonHelper.extractStringNamed(SLR_TYPE, element);
if (StringUtils.isNotBlank(slrType)) {
v.reset().parameter(SLR_TYPE).value(slrType).isOneOfTheseValues(SLR_TYPES.toArray());
}
final String bondType = create ? this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element) : existingBondType;
final boolean debt = bondType != null && DEBT_BOND_TYPES.contains(bondType);
final String isin = this.fromApiJsonHelper.extractStringNamed(ISIN, element);
if (create && debt) {
v.reset().parameter(ISIN).value(isin).notBlank();
}
v.reset().parameter(ISIN).value(isin).ignoreIfNull().notExceedingLengthOf(20);
if (this.fromApiJsonHelper.parameterExists(INT_RATE, element) || (create && debt)) {
final BigDecimal intRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INT_RATE, element);
final DataValidatorBuilder rate = v.reset().parameter(INT_RATE).value(intRate);
if (create && debt) {
rate.notNull();
}
rate.ignoreIfNull().zeroOrPositiveAmount();
}
if (this.fromApiJsonHelper.parameterExists(FACE_VALUE_PER_UNIT, element)) {
final BigDecimal fv = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(FACE_VALUE_PER_UNIT, element);
v.reset().parameter(FACE_VALUE_PER_UNIT).value(fv).ignoreIfNull().positiveAmount();
}
LocalDate issueDate = null;
if (this.fromApiJsonHelper.parameterExists(ISSUE_DATE, element) || (create && debt)) {
issueDate = this.fromApiJsonHelper.extractLocalDateNamed(ISSUE_DATE, element);
if (create && debt) {
v.reset().parameter(ISSUE_DATE).value(issueDate).notNull();
}
}
if (this.fromApiJsonHelper.parameterExists(MATURITY_DATE, element) || (create && debt)) {
final LocalDate maturityDate = this.fromApiJsonHelper.extractLocalDateNamed(MATURITY_DATE, element);
final DataValidatorBuilder maturity = v.reset().parameter(MATURITY_DATE).value(maturityDate);
if (debt) {
// Interest, sale, redemption and amortisation all run to the maturity date.
maturity.notNull();
}
maturity.ignoreIfNull().validateDateAfter(issueDate);
}
validateOptionalString(element, v, COMPANY_NAME, 150);
validateOptionalString(element, v, CORP_CODE, 30);
validateRating(element, v, create && bondType != null && RATED_BOND_TYPES.contains(bondType));
validateAddress(element, v);
throwIfErrors(errors);
}
/** Corporate bonds, CPs and CDs carry their credit rating, so a purchase can be checked against the RBI minimum. */
private void validateRating(final JsonElement element, final DataValidatorBuilder v, final boolean required) {
final String rating = this.fromApiJsonHelper.extractStringNamed(RATING, element);
if (required) {
v.reset().parameter(RATING).value(rating).notBlank();
}
if (StringUtils.isNotBlank(rating)) {
v.reset().parameter(RATING).value(rating).isOneOfTheseValues(RATINGS.toArray());
}
final String agency = this.fromApiJsonHelper.extractStringNamed(RATING_AGENCY, element);
if (StringUtils.isNotBlank(agency)) {
v.reset().parameter(RATING_AGENCY).value(agency).isOneOfTheseValues(RATING_AGENCIES.toArray());
}
if (this.fromApiJsonHelper.parameterExists(LISTED, element)) {
v.reset().parameter(LISTED).value(this.fromApiJsonHelper.extractBooleanNamed(LISTED, element)).notNull();
}
}
// -------------------- shared --------------------
private void validateBondType(final JsonElement element, final DataValidatorBuilder v, final boolean required) {
final String bondType = this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element);
if (required) {
v.reset().parameter(BOND_TYPE).value(bondType).notBlank();
}
if (StringUtils.isNotBlank(bondType)) {
v.reset().parameter(BOND_TYPE).value(bondType).isOneOfTheseValues(BOND_TYPES.toArray());
}
}
private void validateGlAccountId(final JsonElement element, final DataValidatorBuilder v, final boolean required) {
if (!required && !this.fromApiJsonHelper.parameterExists(GL_ACCOUNT_ID, element)) {
return;
}
final Long glAccountId = this.fromApiJsonHelper.extractLongNamed(GL_ACCOUNT_ID, element);
v.reset().parameter(GL_ACCOUNT_ID).value(glAccountId).notNull().longGreaterThanZero();
}
private void validateRequiredString(final JsonElement element, final DataValidatorBuilder v, final String param, final int maxLength,
final boolean required) {
if (!required && !this.fromApiJsonHelper.parameterExists(param, element)) {
return;
}
final String value = this.fromApiJsonHelper.extractStringNamed(param, element);
v.reset().parameter(param).value(value).notBlank().notExceedingLengthOf(maxLength);
}
private void validateOptionalString(final JsonElement element, final DataValidatorBuilder v, final String param,
final int maxLength) {
final String value = this.fromApiJsonHelper.extractStringNamed(param, element);
v.reset().parameter(param).value(value).ignoreIfNull().notExceedingLengthOf(maxLength);
}
private void validateAddress(final JsonElement element, final DataValidatorBuilder v) {
validateOptionalString(element, v, ADDRESS1, 150);
validateOptionalString(element, v, ADDRESS2, 150);
validateOptionalString(element, v, ADDRESS3, 150);
validateOptionalString(element, v, CITY, 100);
validateOptionalString(element, v, PHONE, 20);
validateOptionalString(element, v, FAX, 20);
final String pincode = this.fromApiJsonHelper.extractStringNamed(PINCODE, element);
if (StringUtils.isNotBlank(pincode)) {
v.reset().parameter(PINCODE).value(pincode.trim()).matchesRegularExpression(PINCODE_PATTERN, "Pincode must be 6 digits");
}
}
private JsonElement parse(final String json, final Set<String> supportedParams) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParams);
return this.fromApiJsonHelper.parse(json);
}
private void throwIfErrors(final List<ApiParameterError> errors) {
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
}
@@ -0,0 +1,142 @@
/**
* 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.serialization;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BOND_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BROKERAGE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CATEGORIES;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CATEGORY;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CONSIDER_AMT;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CR_GL_ACCOUNT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DEAL_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DEAL_TIME;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.FACE_VALUE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.LAST_INT_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.NAV;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_CHARGES;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_DETAILS;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARTY_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PURCHASE_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.RESOURCE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SCRIPT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPES;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STAMP_DUTY;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SUB_TYPE;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.investment.api.InvestmentApiConstants;
import org.springframework.stereotype.Component;
/** Validates a Purchase Entry before anything is computed or written. */
@Component
@RequiredArgsConstructor
public final class InvestmentPurchaseCommandFromApiJsonDeserializer {
private static final String TIME_PATTERN = "^([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$";
private final FromJsonHelper fromApiJsonHelper;
public void validateForCreate(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, PARAMS);
final JsonElement element = this.fromApiJsonHelper.parse(json);
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
final String bondType = this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element);
v.reset().parameter(BOND_TYPE).value(bondType).notBlank();
if (StringUtils.isNotBlank(bondType)) {
v.reset().parameter(BOND_TYPE).value(bondType).isOneOfTheseValues(InvestmentApiConstants.BOND_TYPES.toArray());
}
v.reset().parameter(SCRIPT_ID).value(this.fromApiJsonHelper.extractLongNamed(SCRIPT_ID, element)).notNull().longGreaterThanZero();
v.reset().parameter(PARTY_ID).value(this.fromApiJsonHelper.extractLongNamed(PARTY_ID, element)).notNull().longGreaterThanZero();
v.reset().parameter(CR_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(CR_GL_ACCOUNT_ID, element)).notNull()
.longGreaterThanZero();
v.reset().parameter(DEAL_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(DEAL_DATE, element)).notNull();
final String settlementType = this.fromApiJsonHelper.extractStringNamed(SETTLEMENT_TYPE, element);
v.reset().parameter(SETTLEMENT_TYPE).value(settlementType).notBlank();
if (StringUtils.isNotBlank(settlementType)) {
v.reset().parameter(SETTLEMENT_TYPE).value(settlementType).isOneOfTheseValues(SETTLEMENT_TYPES.toArray());
}
final String category = this.fromApiJsonHelper.extractStringNamed(CATEGORY, element);
v.reset().parameter(CATEGORY).value(category).notBlank();
if (StringUtils.isNotBlank(category)) {
v.reset().parameter(CATEGORY).value(category).isOneOfTheseValues(CATEGORIES.toArray());
}
if ("MUTUAL_FUND".equals(bondType)) {
v.reset().parameter(CONSIDER_AMT).value(amount(CONSIDER_AMT, element)).notNull().positiveAmount();
v.reset().parameter(NAV).value(amount(NAV, element)).notNull().positiveAmount();
} else {
v.reset().parameter(FACE_VALUE).value(amount(FACE_VALUE, element)).notNull().positiveAmount();
v.reset().parameter(PURCHASE_RATE).value(amount(PURCHASE_RATE, element)).notNull().positiveAmount();
v.reset().parameter(LAST_INT_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(LAST_INT_DATE, element)).notNull();
}
v.reset().parameter(BROKERAGE).value(amount(BROKERAGE, element)).ignoreIfNull().zeroOrPositiveAmount();
v.reset().parameter(STAMP_DUTY).value(amount(STAMP_DUTY, element)).ignoreIfNull().zeroOrPositiveAmount();
v.reset().parameter(OTHER_CHARGES).value(amount(OTHER_CHARGES, element)).ignoreIfNull().zeroOrPositiveAmount();
final String dealTime = this.fromApiJsonHelper.extractStringNamed(DEAL_TIME, element);
if (StringUtils.isNotBlank(dealTime)) {
v.reset().parameter(DEAL_TIME).value(dealTime).matchesRegularExpression(TIME_PATTERN, "Deal Time must be HH:mm:ss (24 hr)");
}
v.reset().parameter(SUB_TYPE).value(this.fromApiJsonHelper.extractStringNamed(SUB_TYPE, element)).ignoreIfNull()
.notExceedingLengthOf(40);
if (this.fromApiJsonHelper.parameterExists(OTHER_DETAILS, element)) {
final JsonElement other = element.getAsJsonObject().get(OTHER_DETAILS);
if (!other.isJsonNull() && !other.isJsonObject()) {
v.reset().parameter(OTHER_DETAILS).failWithCode("must.be.an.object");
} else if (other.isJsonObject() && other.toString().length() > 4000) {
v.reset().parameter(OTHER_DETAILS).failWithCode("too.long");
}
}
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
private BigDecimal amount(final String param, final JsonElement element) {
return this.fromApiJsonHelper.parameterExists(param, element)
? this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(param, element)
: null;
}
}
@@ -0,0 +1,126 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.serialization;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.AMOUNT;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.BANK_GL_ACCOUNT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.BROKERAGE;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.DEAL_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PARTY_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PURCHASE_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.REDEEM_PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.REMARKS;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.RESOURCE;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SALE_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SANCTION_AUTHORITY;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SELL_PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SETTLEMENT_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.UNITS;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.WRITEOFF_PARAMS;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants;
import org.springframework.stereotype.Component;
/** Validates a sale entry and a redemption request. */
@Component
@RequiredArgsConstructor
public final class InvestmentSaleCommandFromApiJsonDeserializer {
private final FromJsonHelper fromApiJsonHelper;
public void validateForSell(final String json) {
final JsonElement element = parse(json, SELL_PARAMS);
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
v.reset().parameter(PURCHASE_ID).value(this.fromApiJsonHelper.extractLongNamed(PURCHASE_ID, element)).notNull()
.longGreaterThanZero();
v.reset().parameter(PARTY_ID).value(this.fromApiJsonHelper.extractLongNamed(PARTY_ID, element)).notNull().longGreaterThanZero();
v.reset().parameter(BANK_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(BANK_GL_ACCOUNT_ID, element)).notNull()
.longGreaterThanZero();
v.reset().parameter(DEAL_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(DEAL_DATE, element)).notNull();
final String settlementType = this.fromApiJsonHelper.extractStringNamed(SETTLEMENT_TYPE, element);
v.reset().parameter(SETTLEMENT_TYPE).value(settlementType).notBlank();
if (StringUtils.isNotBlank(settlementType)) {
v.reset().parameter(SETTLEMENT_TYPE).value(settlementType)
.isOneOfTheseValues(InvestmentPurchaseApiConstants.SETTLEMENT_TYPES.toArray());
}
v.reset().parameter(UNITS).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(UNITS, element)).notNull()
.positiveAmount();
v.reset().parameter(SALE_RATE).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(SALE_RATE, element)).notNull()
.positiveAmount();
v.reset().parameter(BROKERAGE).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(BROKERAGE, element)).ignoreIfNull()
.zeroOrPositiveAmount();
v.reset().parameter(SANCTION_AUTHORITY).value(this.fromApiJsonHelper.extractStringNamed(SANCTION_AUTHORITY, element))
.ignoreIfNull().notExceedingLengthOf(100);
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, element)).ignoreIfNull()
.notExceedingLengthOf(500);
throwIfAny(errors);
}
public void validateForRedeem(final String json) {
final JsonElement element = parse(json, REDEEM_PARAMS);
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
v.reset().parameter(BANK_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(BANK_GL_ACCOUNT_ID, element)).ignoreIfNull()
.longGreaterThanZero();
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, element)).ignoreIfNull()
.notExceedingLengthOf(500);
throwIfAny(errors);
}
public void validateForWriteOff(final String json) {
final JsonElement element = parse(json, WRITEOFF_PARAMS);
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
v.reset().parameter(AMOUNT).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(AMOUNT, element)).notNull()
.positiveAmount();
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, element)).notBlank()
.notExceedingLengthOf(500);
throwIfAny(errors);
}
private JsonElement parse(final String json, final Set<String> params) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, params);
return this.fromApiJsonHelper.parse(json);
}
private static void throwIfAny(final List<ApiParameterError> errors) {
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
}
@@ -0,0 +1,304 @@
/**
* 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.service;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_ACTIVE;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.AMORTISE_PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.FY_END;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PREMIUM_AMORTISATION_GL_CODE;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PURCHASE_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.STATUS_SETTLED;
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.TYPE_AMORTISE;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.Month;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
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.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.investment.data.InvestmentAmortisationData;
import org.apache.fineract.portfolio.investment.data.InvestmentAmortisationDueData;
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
import org.apache.fineract.portfolio.investment.domain.InvestmentSale;
import org.apache.fineract.portfolio.investment.domain.InvestmentSaleRepository;
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Premium amortisation of HTM holdings: the premium paid over face value is written off straight-line, day by day, over
* the period remaining to maturity (RBI; method and 31 March timing confirmed with Jitendra Sir). Each 31 March posts the
* share for the days since the last amortisation (or settlement): unamortised premium x days to 31 March / days to
* maturity — so a part sale, which takes its share of premium with it, is followed automatically. The remainder after
* the last 31 March is written off with the redemption. Entry: Dr 500604 Amortisation of Premium / Cr Premium GL.
*/
@Service
@RequiredArgsConstructor
public class InvestmentAmortisationService {
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private static final String HTM_PREFIX = "HTM";
private final FromJsonHelper fromApiJsonHelper;
private final InvestmentPurchaseRepository purchaseRepository;
private final InvestmentSaleRepository saleRepository;
private final InvestmentSaleQuoter quoter;
private final InvestmentJournalPoster journalPoster;
@Transactional(readOnly = true)
public InvestmentAmortisationData schedule(final Long purchaseId) {
final InvestmentPurchase p = purchase(purchaseId);
final List<InvestmentSale> posted = amortisations(p);
final LocalDate today = DateUtils.getBusinessLocalDate();
final List<InvestmentAmortisationData.Row> rows = new ArrayList<>();
BigDecimal premium = this.quoter.unamortisedPremium(p);
LocalDate start = p.getSettlementDate();
boolean nextFound = false;
for (final LocalDate fyEnd : yearEnds(p)) {
final Optional<InvestmentSale> done = posted.stream().filter(x -> x.getDealDate().equals(fyEnd)).findFirst();
final long days = ChronoUnit.DAYS.between(start, fyEnd);
if (done.isPresent()) {
rows.add(row("YEAR", fyEnd, start, days, done.get().getBookValueSold(), null, true, done.get(), false));
} else {
final BigDecimal amount = share(premium, start, fyEnd, p.getMaturityDate());
premium = premium.subtract(amount);
final boolean postable = STATUS_ACTIVE.equals(p.getStatus()) && !nextFound && !fyEnd.isAfter(today);
nextFound = true;
rows.add(row("YEAR", fyEnd, start, days, amount, premium, false, null, postable));
}
start = fyEnd;
}
if (premium.signum() > 0 && STATUS_ACTIVE.equals(p.getStatus())) {
rows.add(row("MATURITY", p.getMaturityDate(), start, ChronoUnit.DAYS.between(start, p.getMaturityDate()), premium,
BigDecimal.ZERO, false, null, false));
}
return InvestmentAmortisationData.builder().purchaseId(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo())
.scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc()).category(p.getCategory())
.settlementDate(p.getSettlementDate().format(DATE)).maturityDate(p.getMaturityDate().format(DATE))
.faceValue(p.getBalanceUnits().multiply(p.getScript().getFaceValuePerUnit()))
.premium(p.getPremiumDiscount() == null ? BigDecimal.ZERO : p.getPremiumDiscount().max(BigDecimal.ZERO))
.unamortisedPremium(this.quoter.unamortisedPremium(p)).bookValue(this.quoter.remainingBook(p))
.premiumGlCode(p.getPremiumGlAccount() != null ? p.getPremiumGlAccount().getGlCode() : p.getInvestmentGlAccount().getGlCode())
.rows(rows).build();
}
/** HTM holdings with premium whose next amortisation falls on {@code fyEnd} (Amortize Yearly Process list). */
@Transactional(readOnly = true)
public List<InvestmentAmortisationDueData> due(final LocalDate fyEnd) {
final List<InvestmentAmortisationDueData> due = new ArrayList<>();
for (final InvestmentPurchase p : this.purchaseRepository.findAll(Sort.by(Sort.Direction.ASC, "id"))) {
if (eligible(p) && fyEnd.equals(nextYearEnd(p))) {
final LocalDate start = periodStart(p);
final BigDecimal premium = this.quoter.unamortisedPremium(p);
due.add(InvestmentAmortisationDueData.builder().purchaseId(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo())
.scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc()).fyEnd(fyEnd.format(DATE))
.fromDate(start.format(DATE)).days(ChronoUnit.DAYS.between(start, fyEnd)).unamortisedPremium(premium)
.amount(share(premium, start, fyEnd, p.getMaturityDate())).build());
}
}
return due;
}
/**
* Posts the amortisation for {@code fyEnd} (a 31 March on or before today): for one holding when purchaseId is
* given (Amortize Entry), else for every holding due that year (Amortize Yearly Process). One JV per holding.
*/
@Transactional
public CommandProcessingResult amortise(final JsonCommand command) {
validate(command.json());
final LocalDate fyEnd = command.localDateValueOfParameterNamed(FY_END);
if (fyEnd.getMonth() != Month.MARCH || fyEnd.getDayOfMonth() != 31) {
throw InvestmentSaleQuoter.rule("amortise.not.yearend", "Amortisation is done as on 31 March");
}
if (fyEnd.isAfter(DateUtils.getBusinessLocalDate())) {
throw InvestmentSaleQuoter.rule("amortise.future", "Amortisation for " + fyEnd.format(DATE) + " can be done on or after that date");
}
final Long purchaseId = command.longValueOfParameterNamed(PURCHASE_ID);
final List<InvestmentPurchase> targets = new ArrayList<>();
if (purchaseId != null) {
final InvestmentPurchase p = lockedPurchase(purchaseId);
if (!eligible(p)) {
throw InvestmentSaleQuoter.rule("amortise.not.eligible",
"Deal " + p.getDealNo() + " has no premium left to amortise (only active HTM holdings bought above face value)");
}
final LocalDate next = nextYearEnd(p);
if (!fyEnd.equals(next)) {
throw InvestmentSaleQuoter.rule("amortise.out.of.order", next == null ? "Deal " + p.getDealNo() + " has no 31 March to amortise"
: "Amortise deal " + p.getDealNo() + " year by year: the next is " + next.format(DATE));
}
targets.add(p);
} else {
for (final InvestmentPurchase found : this.purchaseRepository.findAll(Sort.by(Sort.Direction.ASC, "id"))) {
if (eligible(found) && fyEnd.equals(nextYearEnd(found))) {
// Lock, then check again: another run may have posted this year meanwhile.
final InvestmentPurchase p = lockedPurchase(found.getId());
if (eligible(p) && fyEnd.equals(nextYearEnd(p))) {
targets.add(p);
}
}
}
if (targets.isEmpty()) {
throw InvestmentSaleQuoter.rule("amortise.nothing.due", "No holding has premium amortisation due for " + fyEnd.format(DATE));
}
}
final List<String> vouchers = new ArrayList<>();
Long lastId = null;
for (final InvestmentPurchase p : targets) {
final InvestmentSale s = post(p, fyEnd);
if (s.getVoucherNo() != null) {
vouchers.add(s.getVoucherNo());
}
lastId = s.getId();
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(lastId)
.withResourceIdAsString(String.join(", ", vouchers)).build();
}
private InvestmentSale post(final InvestmentPurchase p, final LocalDate fyEnd) {
final LocalDate start = periodStart(p);
final BigDecimal amount = share(this.quoter.unamortisedPremium(p), start, fyEnd, p.getMaturityDate());
final String fy = (fyEnd.getYear() - 1) + "-" + String.format("%02d", fyEnd.getYear() % 100);
// A share that rounds to nothing is still recorded, so the year is done and the next one can follow; it needs no JV.
final CommandProcessingResult posted = amount.signum() == 0 ? null
: this.journalPoster.post(fyEnd, p.getDealNo(),
"Premium amortisation FY " + fy + ": deal " + p.getDealNo() + ", " + p.getScript().getScriptCode() + " "
+ p.getScript().getScriptDesc() + " (" + ChronoUnit.DAYS.between(start, fyEnd) + " days)",
new InvestmentJournalPoster.Lines().debit(this.journalPoster.glId(PREMIUM_AMORTISATION_GL_CODE), amount)
.credit(InvestmentSaleQuoter.premiumGlId(p), amount));
final InvestmentSale s = new InvestmentSale();
s.setPurchase(p);
s.setSaleType(TYPE_AMORTISE);
s.setDealDate(fyEnd);
s.setSettlementDate(fyEnd);
s.setUnits(BigDecimal.ZERO);
s.setFaceValue(BigDecimal.ZERO);
s.setConsideration(BigDecimal.ZERO);
s.setAccruedDays((int) ChronoUnit.DAYS.between(start, fyEnd));
s.setAccruedInterest(BigDecimal.ZERO);
s.setReceivableCleared(BigDecimal.ZERO);
s.setBrokerage(BigDecimal.ZERO);
s.setBookValueSold(amount);
s.setPremiumCleared(amount);
s.setProfitLoss(amount.negate());
s.setNetAmount(BigDecimal.ZERO);
s.setBankGlAccount(p.getCrGlAccount());
s.setRemarks("Premium amortisation FY " + fy);
s.setStatus(STATUS_SETTLED);
s.setGlTransactionId(posted == null ? null : posted.getTransactionId());
final Object voucherNo = posted == null || posted.getChanges() == null ? null : posted.getChanges().get("voucherNo");
s.setVoucherNo(voucherNo == null ? null : voucherNo.toString());
s.setSettledOnDate(DateUtils.getBusinessLocalDate());
return this.saleRepository.saveAndFlush(s);
}
/** Straight-line by days: the share of the unamortised premium for start..yearEnd out of start..maturity. */
static BigDecimal share(final BigDecimal premium, final LocalDate start, final LocalDate yearEnd, final LocalDate maturity) {
final long total = ChronoUnit.DAYS.between(start, maturity);
if (premium.signum() <= 0 || total <= 0) {
return BigDecimal.ZERO;
}
return premium.multiply(BigDecimal.valueOf(ChronoUnit.DAYS.between(start, yearEnd))).divide(BigDecimal.valueOf(total), 2,
RoundingMode.HALF_UP);
}
private boolean eligible(final InvestmentPurchase p) {
return STATUS_ACTIVE.equals(p.getStatus()) && p.getCategory() != null && p.getCategory().startsWith(HTM_PREFIX)
&& p.getMaturityDate() != null && this.quoter.unamortisedPremium(p).signum() > 0;
}
/** The first 31 March after the last amortisation (or the settlement) that is still before maturity. */
private LocalDate nextYearEnd(final InvestmentPurchase p) {
final LocalDate start = periodStart(p);
return yearEnds(p).stream().filter(d -> d.isAfter(start)).findFirst().orElse(null);
}
private LocalDate periodStart(final InvestmentPurchase p) {
return amortisations(p).stream().map(InvestmentSale::getDealDate).max(LocalDate::compareTo).orElse(p.getSettlementDate());
}
/** 31 March dates after the settlement and before maturity. */
private static List<LocalDate> yearEnds(final InvestmentPurchase p) {
final List<LocalDate> ends = new ArrayList<>();
for (int y = p.getSettlementDate().getYear(); y <= p.getMaturityDate().getYear(); y++) {
final LocalDate d = LocalDate.of(y, Month.MARCH, 31);
if (d.isAfter(p.getSettlementDate()) && d.isBefore(p.getMaturityDate())) {
ends.add(d);
}
}
return ends;
}
private List<InvestmentSale> amortisations(final InvestmentPurchase p) {
return this.saleRepository.findByPurchaseIdOrderByIdAsc(p.getId()).stream()
.filter(s -> TYPE_AMORTISE.equals(s.getSaleType()) && STATUS_SETTLED.equals(s.getStatus())).toList();
}
private static InvestmentAmortisationData.Row row(final String kind, final LocalDate end, final LocalDate start, final long days,
final BigDecimal amount, final BigDecimal after, final boolean posted, final InvestmentSale done, final boolean postable) {
return InvestmentAmortisationData.Row.builder().kind(kind).fyEnd(end.format(DATE)).fromDate(start.format(DATE))
.toDate(end.format(DATE)).days(days).amount(amount).premiumAfter(after).posted(posted)
.voucherNo(done == null ? null : done.getVoucherNo()).glTransactionId(done == null ? null : done.getGlTransactionId())
.postable(postable).build();
}
private InvestmentPurchase lockedPurchase(final Long purchaseId) {
return this.purchaseRepository.findByIdWithLock(purchaseId).orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
}
private InvestmentPurchase purchase(final Long purchaseId) {
return this.purchaseRepository.findById(purchaseId).orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
}
private void validate(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, AMORTISE_PARAMS);
final JsonElement element = this.fromApiJsonHelper.parse(json);
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource("investmentsale");
v.reset().parameter(FY_END).value(this.fromApiJsonHelper.extractLocalDateNamed(FY_END, element)).notNull();
v.reset().parameter(PURCHASE_ID).value(this.fromApiJsonHelper.extractLongNamed(PURCHASE_ID, element)).ignoreIfNull()
.longGreaterThanZero();
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
}
@@ -0,0 +1,153 @@
/**
* 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.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.investment.api.InvestmentApiConstants;
import org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants;
import org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants;
import org.apache.fineract.portfolio.investment.data.InvestmentBucketReportData;
import org.apache.fineract.portfolio.investment.domain.InvestmentFd;
import org.apache.fineract.portfolio.investment.domain.InvestmentFdRepository;
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
import org.apache.fineract.portfolio.investment.domain.InvestmentSale;
import org.apache.fineract.portfolio.investment.domain.InvestmentSaleRepository;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Bucket Wise Investment. Every Govt Security, bond and FD held on the as-on date goes into a bucket by the time left
* to its maturity; one already past maturity but not yet redeemed or closed counts in the first bucket.
*/
@Service
@RequiredArgsConstructor
public class InvestmentBucketReportService {
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private static final String GOVT = "GOVT_SECURITY";
private static final String BOND = "BOND";
private static final String FD = "FD";
private static final String[][] BUCKETS = { { "UPTO_1Y", "Up to 1 year" }, { "1Y_3Y", "Over 1 year to 3 years" },
{ "3Y_5Y", "Over 3 years to 5 years" }, { "ABOVE_5Y", "Over 5 years" } };
private final InvestmentPurchaseRepository purchaseRepository;
private final InvestmentSaleRepository saleRepository;
private final InvestmentFdRepository fdRepository;
@Transactional(readOnly = true)
public InvestmentBucketReportData report(final LocalDate asOn) {
final List<InvestmentBucketReportData.Row> rows = new ArrayList<>();
for (final InvestmentPurchase p : this.purchaseRepository.findAll(Sort.by(Sort.Direction.ASC, "maturityDate", "id"))) {
security(p, asOn, rows);
}
for (final InvestmentFd f : this.fdRepository.findAll(Sort.by(Sort.Direction.ASC, "maturityDate", "id"))) {
final boolean held = !f.getDealDate().isAfter(asOn) && (f.getClosedOnDate() == null || f.getClosedOnDate().isAfter(asOn));
if (held) {
rows.add(row(asOn, FD, f.getId(), f.getDealNo(), f.getParty().getPartyName() + " · " + f.getCertNo(), null, f.getIntRate(),
f.getMaturityDate(), f.getPrincipal(), f.getPrincipal()));
}
}
rows.sort(Comparator.comparingLong(InvestmentBucketReportData.Row::getDaysToMaturity));
final List<InvestmentBucketReportData.Bucket> buckets = new ArrayList<>();
for (final String[] b : BUCKETS) {
buckets.add(bucket(b[0], b[1], rows.stream().filter(r -> r.getBucket().equals(b[0])).toList()));
}
return InvestmentBucketReportData.builder().asOnDate(asOn.format(DATE)).buckets(buckets).total(bucket("TOTAL", "Total", rows))
.rows(rows).build();
}
/**
* A security counts when settled on or before the as-on date with units still held on it: units sold or redeemed
* after that date are added back, and book value is cost less what sales, amortisation and write-offs settled by
* then took off.
*/
private void security(final InvestmentPurchase p, final LocalDate asOn, final List<InvestmentBucketReportData.Row> rows) {
final String bondType = p.getBondType() != null ? p.getBondType() : p.getScript().getBondType();
if (!InvestmentApiConstants.DEBT_BOND_TYPES.contains(bondType) || p.getMaturityDate() == null
|| InvestmentPurchaseApiConstants.STATUS_DROPPED.equals(p.getStatus())
|| InvestmentPurchaseApiConstants.STATUS_PENDING_SETTLEMENT.equals(p.getStatus()) || p.getSettlementDate().isAfter(asOn)) {
return;
}
BigDecimal units = p.getBalanceUnits();
BigDecimal book = p.getBookValue();
for (final InvestmentSale s : this.saleRepository.findByPurchaseIdOrderByIdAsc(p.getId())) {
if (!InvestmentSaleApiConstants.STATUS_SETTLED.equals(s.getStatus())) {
continue;
}
if (s.getSettlementDate().isAfter(asOn)) {
units = units.add(s.getUnits());
} else {
book = book.subtract(s.getBookValueSold() == null ? BigDecimal.ZERO : s.getBookValueSold());
}
}
if (units.signum() <= 0) {
return;
}
rows.add(row(asOn, GOVT.equals(bondType) ? GOVT : BOND, p.getId(), p.getDealNo(),
p.getScript().getScriptCode() + " " + p.getScript().getScriptDesc(), p.getCategory(), p.getIntRate(), p.getMaturityDate(),
units.multiply(p.getScript().getFaceValuePerUnit()), book));
}
private static InvestmentBucketReportData.Row row(final LocalDate asOn, final String instrument, final Long refId, final String dealNo,
final String particulars, final String category, final BigDecimal rate, final LocalDate maturity, final BigDecimal face,
final BigDecimal book) {
return InvestmentBucketReportData.Row.builder().bucket(bucketOf(asOn, maturity)).instrument(instrument).refId(refId).dealNo(dealNo)
.particulars(particulars).category(category).rate(rate).maturityDate(maturity.format(DATE))
.daysToMaturity(ChronoUnit.DAYS.between(asOn, maturity)).faceValue(face).bookValue(book).build();
}
private static String bucketOf(final LocalDate asOn, final LocalDate maturity) {
if (!maturity.isAfter(asOn.plusYears(1))) {
return BUCKETS[0][0];
}
if (!maturity.isAfter(asOn.plusYears(3))) {
return BUCKETS[1][0];
}
return maturity.isAfter(asOn.plusYears(5)) ? BUCKETS[3][0] : BUCKETS[2][0];
}
private static InvestmentBucketReportData.Bucket bucket(final String code, final String label,
final List<InvestmentBucketReportData.Row> rows) {
final List<InvestmentBucketReportData.Row> govt = rows.stream().filter(r -> GOVT.equals(r.getInstrument())).toList();
final List<InvestmentBucketReportData.Row> bond = rows.stream().filter(r -> BOND.equals(r.getInstrument())).toList();
final List<InvestmentBucketReportData.Row> fd = rows.stream().filter(r -> FD.equals(r.getInstrument())).toList();
final BigDecimal govtBook = sum(govt, InvestmentBucketReportData.Row::getBookValue);
final BigDecimal bondBook = sum(bond, InvestmentBucketReportData.Row::getBookValue);
final BigDecimal fdPrincipal = sum(fd, InvestmentBucketReportData.Row::getBookValue);
return InvestmentBucketReportData.Bucket.builder().code(code).label(label).govtCount(govt.size())
.govtFaceValue(sum(govt, InvestmentBucketReportData.Row::getFaceValue)).govtBookValue(govtBook).bondCount(bond.size())
.bondFaceValue(sum(bond, InvestmentBucketReportData.Row::getFaceValue)).bondBookValue(bondBook).fdCount(fd.size())
.fdPrincipal(fdPrincipal).totalAmount(govtBook.add(bondBook).add(fdPrincipal)).build();
}
private static BigDecimal sum(final List<InvestmentBucketReportData.Row> rows,
final java.util.function.Function<InvestmentBucketReportData.Row, BigDecimal> f) {
return rows.stream().map(f).reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
@@ -0,0 +1,56 @@
/**
* 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.service;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.investment.data.InvestmentDailyRateData;
import org.apache.fineract.portfolio.investment.domain.InvestmentDailyRateRepository;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class InvestmentDailyRateReadPlatformService {
/** The register shows the latest rates first; older ones are reached by picking a script. */
private static final int MAX_ROWS = 500;
private final InvestmentDailyRateRepository dailyRateRepository;
public List<InvestmentDailyRateData> retrieveAll(final Long scriptId) {
if (scriptId != null) {
return this.dailyRateRepository.findByScriptIdOrderByRateDateDesc(scriptId).stream().map(InvestmentDailyRateData::from)
.toList();
}
return this.dailyRateRepository
.findAll(PageRequest.of(0, MAX_ROWS, Sort.by(Sort.Direction.DESC, "rateDate").and(Sort.by(Sort.Direction.DESC, "id"))))
.stream().map(InvestmentDailyRateData::from).toList();
}
/** The newest rate of a script on or before {@code onDate}; null when none has been entered. */
public InvestmentDailyRateData retrieveLatest(final Long scriptId, final LocalDate onDate) {
return this.dailyRateRepository.findFirstByScriptIdAndRateDateLessThanEqualOrderByRateDateDesc(scriptId, onDate)
.map(InvestmentDailyRateData::from).orElse(null);
}
}
@@ -0,0 +1,29 @@
/**
* 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.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface InvestmentDailyRateWritePlatformService {
CommandProcessingResult createDailyRate(JsonCommand command);
CommandProcessingResult deleteDailyRate(Long rateId);
}
@@ -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.service;
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.CLOSE_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.HIGH_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.LOW_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.OPEN_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.RATE_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.SCRIPT_ID;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.portfolio.investment.domain.InvestmentDailyRate;
import org.apache.fineract.portfolio.investment.domain.InvestmentDailyRateRepository;
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
import org.apache.fineract.portfolio.investment.exception.InvestmentDailyRateNotFoundException;
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
import org.apache.fineract.portfolio.investment.serialization.InvestmentDailyRateCommandFromApiJsonDeserializer;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/** Saves and drops Daily Script Master rates. A wrong rate is corrected by dropping it and entering it again. */
@Service
@RequiredArgsConstructor
public class InvestmentDailyRateWritePlatformServiceImpl implements InvestmentDailyRateWritePlatformService {
private final InvestmentDailyRateCommandFromApiJsonDeserializer validator;
private final InvestmentDailyRateRepository dailyRateRepository;
private final InvestmentScriptRepository scriptRepository;
@Override
@Transactional
public CommandProcessingResult createDailyRate(final JsonCommand command) {
this.validator.validateForCreate(command.json());
final Long scriptId = command.longValueOfParameterNamed(SCRIPT_ID);
final InvestmentScript script = this.scriptRepository.findById(scriptId)
.orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
final LocalDate rateDate = command.localDateValueOfParameterNamed(RATE_DATE);
if (this.dailyRateRepository.existsByScriptIdAndRateDate(scriptId, rateDate)) {
throw new GeneralPlatformDomainRuleException("error.msg.investmentdailyrate.duplicate",
"A rate for " + script.getScriptCode() + " on this date already exists. Drop it first to correct it.");
}
final InvestmentDailyRate rate = new InvestmentDailyRate();
rate.setScript(script);
rate.setRateDate(rateDate);
rate.setOpenRate(command.bigDecimalValueOfParameterNamed(OPEN_RATE));
rate.setHighRate(command.bigDecimalValueOfParameterNamed(HIGH_RATE));
rate.setLowRate(command.bigDecimalValueOfParameterNamed(LOW_RATE));
rate.setCloseRate(command.bigDecimalValueOfParameterNamed(CLOSE_RATE));
final InvestmentDailyRate saved = this.dailyRateRepository.saveAndFlush(rate);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
.withResourceIdAsString(script.getScriptCode()).build();
}
@Override
@Transactional
public CommandProcessingResult deleteDailyRate(final Long rateId) {
final InvestmentDailyRate rate = this.dailyRateRepository.findById(rateId)
.orElseThrow(() -> new InvestmentDailyRateNotFoundException(rateId));
this.dailyRateRepository.delete(rate);
this.dailyRateRepository.flush();
return new CommandProcessingResultBuilder().withEntityId(rateId).build();
}
}
@@ -0,0 +1,136 @@
/**
* 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.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants;
import org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants;
import org.apache.fineract.portfolio.investment.data.InvestmentAmortisationData;
import org.apache.fineract.portfolio.investment.data.InvestmentDiaryData;
import org.apache.fineract.portfolio.investment.data.InvestmentFdScheduleData;
import org.apache.fineract.portfolio.investment.data.InvestmentInterestScheduleData;
import org.apache.fineract.portfolio.investment.domain.InvestmentFd;
import org.apache.fineract.portfolio.investment.domain.InvestmentFdRepository;
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Due Date Diary: the dated events of every active investment between two dates, taken from the same schedules the
* entry screens use, so the diary always agrees with Interest Entry, Amortize Entry and the FD screens.
*/
@Service
@RequiredArgsConstructor
public class InvestmentDiaryService {
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private static final String SECURITY = "SECURITY";
private static final String FD = "FD";
/** Events that bring money in; provisions and amortisation are book entries only. */
private static final Set<String> CASH_KINDS = Set.of("COUPON", "MATURITY", "FD_PAYOUT", "FD_MATURITY");
private final InvestmentPurchaseRepository purchaseRepository;
private final InvestmentFdRepository fdRepository;
private final InvestmentPurchaseReadPlatformService purchaseReadPlatformService;
private final InvestmentAmortisationService amortisationService;
private final InvestmentFdInterestService fdInterestService;
@Transactional(readOnly = true)
public InvestmentDiaryData diary(final LocalDate from, final LocalDate to) {
final LocalDate today = DateUtils.getBusinessLocalDate();
final List<InvestmentDiaryData.Row> rows = new ArrayList<>();
for (final InvestmentPurchase p : this.purchaseRepository.findAll(Sort.by(Sort.Direction.ASC, "id"))) {
if (!InvestmentPurchaseApiConstants.STATUS_ACTIVE.equals(p.getStatus())) {
continue;
}
final InvestmentInterestScheduleData s = this.purchaseReadPlatformService.retrieveInterestSchedule(p.getId(), null);
final String particulars = s.getScriptCode() + " " + s.getScriptDesc();
for (final InvestmentInterestScheduleData.Row r : s.getRows()) {
// A coupon shows the cash coming in (the full coupon); a provision shows what it books.
final BigDecimal amount = "COUPON".equals(r.getType()) ? r.getGrossInterest() : r.getAmount();
add(rows, from, to, today, r.getIntDate(), r.getType(), SECURITY, p.getId(), p.getDealNo(), particulars, amount,
r.isPosted(), r.getVoucherNo());
}
if (p.getMaturityDate() != null) {
add(rows, from, to, today, s.getMaturityDate(), "MATURITY", SECURITY, p.getId(), p.getDealNo(), particulars,
s.getFaceValue(), false, null);
}
if (p.getCategory() != null && p.getCategory().startsWith("HTM") && p.getPremiumDiscount() != null
&& p.getPremiumDiscount().signum() > 0) {
final InvestmentAmortisationData a = this.amortisationService.schedule(p.getId());
for (final InvestmentAmortisationData.Row r : a.getRows()) {
if ("YEAR".equals(r.getKind()) && r.getAmount() != null && r.getAmount().signum() > 0) {
add(rows, from, to, today, r.getFyEnd(), "AMORTISATION", SECURITY, p.getId(), p.getDealNo(), particulars,
r.getAmount(), r.isPosted(), r.getVoucherNo());
}
}
}
}
for (final InvestmentFd f : this.fdRepository.findByStatusOrderByIdAsc(InvestmentFdApiConstants.STATUS_ACTIVE)) {
final InvestmentFdScheduleData s = this.fdInterestService.schedule(f.getId());
final String particulars = f.getParty().getPartyName() + " · " + f.getCertNo();
for (final InvestmentFdScheduleData.Row r : s.getRows()) {
final boolean maturity = InvestmentFdApiConstants.LINE_MATURITY.equals(r.getLineType());
// Maturity: the maturity amount; a payout: the interest paid (net of TDS); a provision: what it books.
final BigDecimal amount = maturity ? f.getExpectedMaturityAmount()
: InvestmentFdApiConstants.LINE_PAYOUT.equals(r.getLineType())
? r.getGrossInterest().subtract(r.getTds() == null ? BigDecimal.ZERO : r.getTds())
: r.getAmount();
add(rows, from, to, today, r.getIntDate(), "FD_" + r.getLineType(), FD, f.getId(), f.getDealNo(), particulars, amount,
r.isPosted(), r.getVoucherNo());
}
}
rows.sort(Comparator.comparing((InvestmentDiaryData.Row r) -> LocalDate.parse(r.getDate(), DATE))
.thenComparing(InvestmentDiaryData.Row::getInstrument).thenComparing(InvestmentDiaryData.Row::getDealNo));
final BigDecimal inflow = rows.stream().filter(r -> CASH_KINDS.contains(r.getKind()) && !"POSTED".equals(r.getStatus()))
.map(InvestmentDiaryData.Row::getAmount).filter(a -> a != null).reduce(BigDecimal.ZERO, BigDecimal::add);
return InvestmentDiaryData.builder().fromDate(from.format(DATE)).toDate(to.format(DATE)).asOnDate(today.format(DATE))
.pendingCount((int) rows.stream().filter(r -> !"POSTED".equals(r.getStatus())).count())
.overdueCount((int) rows.stream().filter(r -> "OVERDUE".equals(r.getStatus())).count()).expectedInflow(inflow).rows(rows)
.build();
}
private static void add(final List<InvestmentDiaryData.Row> rows, final LocalDate from, final LocalDate to, final LocalDate today,
final String date, final String kind, final String instrument, final Long refId, final String dealNo, final String particulars,
final BigDecimal amount, final boolean posted, final String voucherNo) {
if (date == null) {
return;
}
final LocalDate d = LocalDate.parse(date, DATE);
if (d.isBefore(from) || d.isAfter(to)) {
return;
}
final String status = posted ? "POSTED" : d.isBefore(today) ? "OVERDUE" : d.isEqual(today) ? "DUE" : "UPCOMING";
rows.add(InvestmentDiaryData.Row.builder().date(date).kind(kind).instrument(instrument).refId(refId).dealNo(dealNo)
.particulars(particulars).amount(amount).status(status).voucherNo(voucherNo).build());
}
}
@@ -0,0 +1,79 @@
/**
* 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.service;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CALC_HALF_YEARLY;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CALC_MONTHLY;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CALC_QUARTERLY;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CALC_SIMPLE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CALC_YEARLY;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.PAYOUT_AT_MATURITY;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.PAYOUT_HALF_YEARLY;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
/**
* Interest on an FD with another bank, on actual days over a 365-day year. A payout FD (interest paid every half
* year / year) earns simple interest; an FD paid at maturity compounds at the chosen frequency, or is simple. The
* user also enters the bank's own figure at maturity, so a different compounding shows up as a difference.
*/
public final class InvestmentFdCalculator {
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
private static final BigDecimal DAYS_IN_YEAR = BigDecimal.valueOf(365);
private InvestmentFdCalculator() {}
public static BigDecimal interest(final BigDecimal principal, final BigDecimal ratePercent, final LocalDate from, final LocalDate to,
final String calcType, final String payout) {
final long days = ChronoUnit.DAYS.between(from, to);
if (days <= 0 || principal == null || ratePercent == null || principal.signum() <= 0 || ratePercent.signum() <= 0) {
return BigDecimal.ZERO;
}
if (!PAYOUT_AT_MATURITY.equals(payout) || CALC_SIMPLE.equals(calcType)) {
return principal.multiply(ratePercent).multiply(BigDecimal.valueOf(days)).divide(HUNDRED.multiply(DAYS_IN_YEAR), 2,
RoundingMode.HALF_UP);
}
final int n = periodsPerYear(calcType);
final double factor = Math.pow(1 + ratePercent.doubleValue() / 100 / n, n * (days / 365.0));
return principal.multiply(BigDecimal.valueOf(factor - 1)).setScale(2, RoundingMode.HALF_UP);
}
/** Interest paid each half year or year on a payout FD; zero for an FD paid at maturity. */
public static BigDecimal periodInterest(final BigDecimal principal, final BigDecimal ratePercent, final String payout) {
if (PAYOUT_AT_MATURITY.equals(payout) || principal == null || ratePercent == null) {
return BigDecimal.ZERO;
}
final BigDecimal perYear = principal.multiply(ratePercent).divide(HUNDRED, 2, RoundingMode.HALF_UP);
return PAYOUT_HALF_YEARLY.equals(payout) ? perYear.divide(BigDecimal.valueOf(2), 2, RoundingMode.HALF_UP) : perYear;
}
private static int periodsPerYear(final String calcType) {
return switch (calcType) {
case CALC_MONTHLY -> 12;
case CALC_QUARTERLY -> 4;
case CALC_HALF_YEARLY -> 2;
case CALC_YEARLY -> 1;
default -> throw new IllegalArgumentException("Unknown calculation type " + calcType);
};
}
}

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