Compare commits

..
Author SHA1 Message Date
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) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Waiting to run
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Waiting to run
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Waiting to run
Fineract Documentation build / build (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Waiting to run
Fineract Liquibase Only mode - PostgreSQL / test (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Waiting to run
2026-10-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
149 changed files with 11361 additions and 2253 deletions
@@ -95,6 +95,198 @@ public class CommandWrapperBuilder {
return this; 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() { public CommandWrapperBuilder createChequeStockLot() {
this.actionName = "CREATE"; this.actionName = "CREATE";
this.entityName = "CHEQUESTOCKLOT"; this.entityName = "CHEQUESTOCKLOT";
-2
View File
@@ -81,8 +81,6 @@ dependencies {
'org.apache.avro:avro', 'org.apache.avro:avro',
'jakarta.ws.rs:jakarta.ws.rs-api', 'jakarta.ws.rs:jakarta.ws.rs-api',
'org.glassfish.jersey.media:jersey-media-multipart', 'org.glassfish.jersey.media:jersey-media-multipart',
// Server-Sent Events (SseEventSink, Sse): the Approvals badge is pushed to the screen over one.
'org.glassfish.jersey.media:jersey-media-sse',
'org.springframework:spring-jms', 'org.springframework:spring-jms',
'org.springframework:spring-context-support', 'org.springframework:spring-context-support',
@@ -1570,76 +1570,9 @@ public class AccountingProcessorHelper {
return mapping == null ? null : mapping.getGlAccount(); return mapping == null ? null : mapping.getGlAccount();
} }
/**
* The clearing account for the savings-to-savings account transfer running on this thread, taken from a product's
* own Transfers In Suspense mapping (see {@link #useProductTransferSuspense}). Null outside such a transfer.
*/
private static final ThreadLocal<GLAccount> TRANSFER_CLEARING_ACCOUNT = new ThreadLocal<>();
/**
* Makes both legs of a savings-to-savings account transfer (savings to RD by auto debit, savings to savings, a
* deposit paying out) clear through the Transfers In Suspense account set on the product's Accounting tab, rather
* than the organisation-wide Liability Transfer financial activity.
* <p>
* The receiving product's mapping is used first, then the paying product's: a savings product screen does not
* offer Transfers In Suspense, so for savings to RD it is the RD product's mapping. Both legs use the same account,
* so it nets to nil. Where neither product has one, nothing is set and the transfer clears through Liability
* Transfer as before.
* <p>
* Must be paired with {@link #clearProductTransferSuspense()} in a finally block.
*
* @return true when a product mapping was found and is now in use
*/
public boolean useProductTransferSuspense(final Long toSavingsProductId, final Long fromSavingsProductId) {
final int transfersSuspense = CashAccountsForSavings.TRANSFERS_SUSPENSE.getValue();
GLAccount account = optionalLinkedGLAccountForSavingsProduct(toSavingsProductId, transfersSuspense, null);
if (account == null) {
account = optionalLinkedGLAccountForSavingsProduct(fromSavingsProductId, transfersSuspense, null);
}
if (account == null) {
TRANSFER_CLEARING_ACCOUNT.remove();
return false;
}
TRANSFER_CLEARING_ACCOUNT.set(account);
return true;
}
public void clearProductTransferSuspense() {
TRANSFER_CLEARING_ACCOUNT.remove();
}
/**
* For undoing one leg of an account transfer: reverses through the clearing account that leg was actually posted
* to, whether a product's Transfers In Suspense or the Liability Transfer financial activity. A reversal works the
* accounts out again rather than copying the original rows, so without this a transfer posted to a product's
* suspense account would be reversed against Liability Transfer and leave both accounts open.
* <p>
* The clearing row is the credit of a withdrawal and the debit of a deposit (the largest, should a fee sit beside
* it). Nothing is set where the leg has no journal entries. Must be paired with
* {@link #clearProductTransferSuspense()} in a finally block.
*/
public void useClearingAccountPostedFor(final Long savingsTransactionId, final boolean isWithdrawal) {
TRANSFER_CLEARING_ACCOUNT.remove();
if (savingsTransactionId == null) {
return;
}
final List<Long> found = jdbcTemplate.queryForList("""
SELECT account_id FROM acc_gl_journal_entry
WHERE savings_transaction_id = ? AND type_enum = ? AND reversed = false
ORDER BY amount DESC, id
LIMIT 1
""", Long.class, savingsTransactionId, isWithdrawal ? JournalEntryType.CREDIT.getValue() : JournalEntryType.DEBIT.getValue());
if (!found.isEmpty()) {
glAccountRepository.findById(found.get(0)).ifPresent(TRANSFER_CLEARING_ACCOUNT::set);
}
}
private GLAccount getLinkedGLAccountForSavingsProduct(final Long savingsProductId, final int accountMappingTypeId, private GLAccount getLinkedGLAccountForSavingsProduct(final Long savingsProductId, final int accountMappingTypeId,
final Long paymentTypeId) { final Long paymentTypeId) {
GLAccount glAccount; GLAccount glAccount;
if (accountMappingTypeId == FinancialActivity.LIABILITY_TRANSFER.getValue() && TRANSFER_CLEARING_ACCOUNT.get() != null) {
return TRANSFER_CLEARING_ACCOUNT.get();
}
if (isOrganizationAccount(accountMappingTypeId)) { if (isOrganizationAccount(accountMappingTypeId)) {
FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository
.findByFinancialActivityTypeWithNotFoundDetection(accountMappingTypeId); .findByFinancialActivityTypeWithNotFoundDetection(accountMappingTypeId);
@@ -144,22 +144,14 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer,
isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), isInterestTransfer, isWithdrawBalance); isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), isInterestTransfer, isWithdrawBalance);
final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, fmt,
transactionDate, transactionAmount, paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill);
final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName); final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill); final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill);
// As in transferFunds: both legs clear through the product's Transfers In Suspense where one is mapped. final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate,
final SavingsAccountTransaction withdrawal; transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
final SavingsAccountTransaction deposit;
this.accountingProcessorHelper.useProductTransferSuspense(toSavingsAccount.productId(), fromSavingsAccount.productId());
try {
withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, fmt, transactionDate, transactionAmount,
paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill);
deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate, transactionAmount,
paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
} finally {
this.accountingProcessorHelper.clearProductTransferSuspense();
}
applyToRecurringDepositSchedule(toSavingsAccount, deposit); applyToRecurringDepositSchedule(toSavingsAccount, deposit);
if (!fromSavingsAccount.getCurrency().getCode().equals(toSavingsAccount.getCurrency().getCode())) { if (!fromSavingsAccount.getCurrency().getCode().equals(toSavingsAccount.getCurrency().getCode())) {
@@ -285,27 +277,14 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
if (accountTransfer.getToLoanTransaction() != null) { if (accountTransfer.getToLoanTransaction() != null) {
this.loanAccountDomainService.reverseTransfer(accountTransfer.getToLoanTransaction()); this.loanAccountDomainService.reverseTransfer(accountTransfer.getToLoanTransaction());
} }
// Each leg is reversed through the clearing account it was posted to (a product's Transfers In Suspense or
// Liability Transfer), so the reversal clears that same account.
if (accountTransfer.getFromTransaction() != null) { if (accountTransfer.getFromTransaction() != null) {
this.accountingProcessorHelper.useClearingAccountPostedFor(accountTransfer.getFromTransaction().getId(), true);
try {
this.savingsAccountWritePlatformService.undoTransaction( this.savingsAccountWritePlatformService.undoTransaction(
accountTransfer.accountTransferDetails().fromSavingsAccount().getId(), accountTransfer.accountTransferDetails().fromSavingsAccount().getId(), accountTransfer.getFromTransaction().getId(),
accountTransfer.getFromTransaction().getId(), true); true);
} finally {
this.accountingProcessorHelper.clearProductTransferSuspense();
}
} }
if (accountTransfer.getToSavingsTransaction() != null) { if (accountTransfer.getToSavingsTransaction() != null) {
this.accountingProcessorHelper.useClearingAccountPostedFor(accountTransfer.getToSavingsTransaction().getId(), false); this.savingsAccountWritePlatformService.undoTransaction(accountTransfer.accountTransferDetails().toSavingsAccount().getId(),
try {
this.savingsAccountWritePlatformService.undoTransaction(
accountTransfer.accountTransferDetails().toSavingsAccount().getId(),
accountTransfer.getToSavingsTransaction().getId(), true); accountTransfer.getToSavingsTransaction().getId(), true);
} finally {
this.accountingProcessorHelper.clearProductTransferSuspense();
}
} }
accountTransfer.reverse(); accountTransfer.reverse();
this.accountTransferRepository.save(accountTransfer); this.accountTransferRepository.save(accountTransfer);
@@ -428,22 +407,13 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
transactionDate = transactionDate.plusDays(1); transactionDate = transactionDate.plusDays(1);
} }
// Both legs clear through the Transfers In Suspense account on the product's Accounting tab (the receiving final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount,
// product's, else the paying one's) instead of the organisation-wide Liability Transfer mapping. accountTransferDTO.getFmt(), transactionDate, accountTransferDTO.getTransactionAmount(),
final SavingsAccountTransaction withdrawal; accountTransferDTO.getPaymentDetail(), transactionBooleanValues, backdatedTxnsAllowedTill);
final SavingsAccountTransaction deposit;
this.accountingProcessorHelper.useProductTransferSuspense(toSavingsAccount.productId(), fromSavingsAccount.productId());
try {
withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, accountTransferDTO.getFmt(),
transactionDate, accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
transactionBooleanValues, backdatedTxnsAllowedTill);
deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, accountTransferDTO.getFmt(), transactionDate, final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount,
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), isAccountTransfer, accountTransferDTO.getFmt(), transactionDate, accountTransferDTO.getTransactionAmount(),
isRegularTransaction, backdatedTxnsAllowedTill); accountTransferDTO.getPaymentDetail(), isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
} finally {
this.accountingProcessorHelper.clearProductTransferSuspense();
}
applyToRecurringDepositSchedule(toSavingsAccount, deposit); applyToRecurringDepositSchedule(toSavingsAccount, deposit);
accountTransferDetails = this.accountTransferAssembler.assembleSavingsToSavingsTransfer(accountTransferDTO, fromSavingsAccount, accountTransferDetails = this.accountTransferAssembler.assembleSavingsToSavingsTransfer(accountTransferDTO, fromSavingsAccount,
@@ -1,67 +0,0 @@
package org.apache.fineract.portfolio.approvals.api;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.DefaultValue;
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.CacheControl;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.sse.Sse;
import jakarta.ws.rs.sse.SseEventSink;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.approvals.service.ApprovalCountsReadPlatformService;
import org.apache.fineract.portfolio.approvals.service.ApprovalCountsStreamService;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.stereotype.Component;
/**
* How many requests wait on every Approval Center tab, for the badge on each tab and the Total on the Approvals menu.
* Numbers only: opening a tab still loads that tab's own list. The counting is in
* {@link ApprovalCountsReadPlatformService}; the live stream is in {@link ApprovalCountsStreamService}.
*/
@Path("/v1/approvals/counts")
@Component
@RequiredArgsConstructor
@Produces({ MediaType.APPLICATION_JSON })
public class ApprovalCountsApiResource {
private final PlatformSecurityContext context;
private final ApprovalCountsReadPlatformService readService;
private final ApprovalCountsStreamService streamService;
/**
* The counts as they are now, answered at once. {@code fresh=true} skips the few seconds the last answer is held
* for. Screens that can stream use {@link #stream} instead; this is for a one-off read and as a fallback.
*/
@GET
public Response counts(@QueryParam("fresh") @DefaultValue("false") final boolean fresh) {
final AppUser user = this.context.authenticatedUser();
final CacheControl noStore = new CacheControl();
noStore.setNoStore(true);
return Response.ok(this.readService.retrieveHeld(user, fresh).toJson()).cacheControl(noStore).build();
}
/**
* Server-Sent Events: the connection stays open and a {@code counts} event is written the moment the numbers change,
* so a screen neither polls nor reloads. The first event carries the numbers as they stand.
*/
@GET
@Path("stream")
@Produces(MediaType.SERVER_SENT_EVENTS)
public void stream(@Context final SseEventSink sink, @Context final Sse sse, @Context final HttpServletResponse response) {
// Ask reverse proxies not to hold the events back.
response.setHeader("X-Accel-Buffering", "no");
response.setHeader("Cache-Control", "no-cache, no-store");
try {
this.streamService.open(this.readService.planFor(this.context.authenticatedUser()), sink, sse);
} catch (final RuntimeException e) {
sink.close();
throw e;
}
}
}
@@ -1,14 +0,0 @@
package org.apache.fineract.portfolio.approvals.data;
/**
* One Approval Center tab and the question that counts what waits on it.
*
* @param key
* the tab's route segment, which is also the key its badge reads on the screen
* @param permission
* the permission the tab is shown under (READ_* is also met by ALL_FUNCTIONS_READ); null means every caller sees
* the tab
* @param sql
* a scalar sub-select answering "how many", written with the named parameters of {@link ApprovalCountsPlan}
*/
public record ApprovalCounter(String key, String permission, String sql) {}
@@ -1,27 +0,0 @@
package org.apache.fineract.portfolio.approvals.data;
import com.google.gson.Gson;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* What is waiting for approval: the total, the number for each tab that has anything, and a version that is equal
* exactly when the numbers are, which is how a change is told from a repeat.
*/
public record ApprovalCountsData(long total, Map<String, Long> counts, String version) {
private static final Gson GSON = new Gson();
public static ApprovalCountsData of(final Map<String, Long> counts) {
final long total = counts.values().stream().mapToLong(Long::longValue).sum();
return new ApprovalCountsData(total, counts, Integer.toHexString(counts.hashCode()));
}
public String toJson() {
final Map<String, Object> json = new LinkedHashMap<>();
json.put("total", this.total);
json.put("counts", this.counts);
json.put("version", this.version);
return GSON.toJson(json);
}
}
@@ -1,22 +0,0 @@
package org.apache.fineract.portfolio.approvals.data;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
/**
* What one caller is counted for: the tabs they may see and the values (office scope, user, checker rights) those
* tabs' queries are asked with. It is worked out on the request thread, because it reads the user's roles, and can then
* be used from any thread.
*
* @param tenant
* the tenant the counts are read from
* @param visible
* the tabs this caller may see, in the order the Approval Center shows them
* @param params
* the named parameters the tabs' queries use
* @param group
* equal for callers who are counted identically, so they can share one query
*/
public record ApprovalCountsPlan(FineractPlatformTenant tenant, List<ApprovalCounter> visible, MapSqlParameterSource params,
String group) {}
@@ -1,122 +0,0 @@
package org.apache.fineract.portfolio.approvals.service;
import java.util.List;
import org.apache.fineract.portfolio.approvals.data.ApprovalCounter;
/**
* Every Approval Center tab that carries a badge, and the question that counts what waits on it. Each question is the
* one the tab asks of its own API (same table, same status, same office scope, same entries left out). To badge a new
* tab, add one line here and one badge on the screen.
*
* The queries use these named parameters, supplied per caller: {@code :h} (office hierarchy pattern), {@code :ho} (head
* office), {@code :uid} (user) and {@code :superChecker} (may check every maker-checker command).
*/
public final class ApprovalCounterRegistry {
private ApprovalCounterRegistry() {}
/** Entities whose maker-checker requests are shown under Product Approval, not in the Checker Inbox. */
private static final String PRODUCT_ENTITIES = "'LOANPRODUCT','SAVINGSPRODUCT','CURRENTPRODUCT','SHAREPRODUCT',"
+ "'FIXEDDEPOSITPRODUCT','RECURRINGDEPOSITPRODUCT','CHARGE'";
/** Product edits that are staged in m_saving_product_temp, so Product Approval already lists them from there. */
private static final String STAGED_PRODUCT_ENTITIES = "'SAVINGSPRODUCT','CURRENTPRODUCT','SHAREPRODUCT',"
+ "'FIXEDDEPOSITPRODUCT','RECURRINGDEPOSITPRODUCT'";
/** Accounts whose UPDATE is staged for Pending Account Edits, so the Checker Inbox leaves it out. */
private static final String STAGED_ACCOUNT_ENTITIES = "'SAVINGSACCOUNT','CURRENTACCOUNT'";
private static final String DDS_ENTITIES = "'PARTY','AGENT','PARTYTRANSACTION','DDSACCOUNTTYPE','COMMISSIONSLAB',"
+ "'DDSBATCH','DDSCOLLECTIONENTRY'";
/** Maker-checker commands awaiting a checker, limited to the offices and checker rights the caller has. */
private static final String MC_PENDING = "FROM m_portfolio_command_source aud WHERE aud.status = 2 "
+ "AND (:ho OR EXISTS (SELECT 1 FROM m_office o2 WHERE o2.id = aud.office_id AND o2.hierarchy LIKE :h)) "
+ "AND (:superChecker OR EXISTS (SELECT 1 FROM m_permission p JOIN m_role_permission rp ON rp.permission_id = p.id "
+ "JOIN m_appuser_role ur ON ur.role_id = rp.role_id AND ur.appuser_id = :uid "
+ "WHERE p.code LIKE '%\\_CHECKER' AND REPLACE(p.action_name, '_CHECKER', '') = aud.action_name "
+ "AND p.entity_name = aud.entity_name)) ";
/** Where the person who raised a loan or account sits, for the office scope the tab applies. */
private static final String CLIENT_OFFICE = "EXISTS (SELECT 1 FROM m_office o WHERE o.id = COALESCE(c.office_id, g.office_id) "
+ "AND o.hierarchy LIKE :h)";
/** The one tab whose list is open to managers and head cashiers only. */
public static final String DISBURSEMENT_REQUESTS = "disbursement-requests";
/** The tabs, in the order the Approval Center shows them. A null permission means every caller sees the tab. */
public static final List<ApprovalCounter> ALL = List.of(
// ── Fineract's own maker-checker and pending lists ────────────────────────────
new ApprovalCounter("checker-inbox", "ALL_FUNCTIONS_READ",
"SELECT count(*) " + MC_PENDING + "AND upper(aud.entity_name) NOT IN (" + PRODUCT_ENTITIES + ") "
+ "AND NOT (upper(aud.entity_name) IN (" + STAGED_ACCOUNT_ENTITIES + ") AND upper(aud.action_name) = 'UPDATE')"),
new ApprovalCounter("client-approval", "READ_CLIENT",
"SELECT count(*) FROM m_client c WHERE c.status_enum = 100 "
+ "AND EXISTS (SELECT 1 FROM m_office o WHERE o.id = c.office_id AND o.hierarchy LIKE :h)"),
new ApprovalCounter("loan-approval", "READ_LOAN",
"SELECT count(*) FROM m_loan l LEFT JOIN m_client c ON c.id = l.client_id LEFT JOIN m_group g ON g.id = l.group_id "
+ "WHERE l.loan_status_id = 100 AND " + CLIENT_OFFICE),
new ApprovalCounter("loan-disbursal", "READ_LOAN",
"SELECT count(*) FROM m_loan l LEFT JOIN m_client c ON c.id = l.client_id LEFT JOIN m_group g ON g.id = l.group_id "
+ "WHERE l.loan_status_id = 200 AND " + CLIENT_OFFICE),
new ApprovalCounter("reschedule-loan", "RESCHEDULE_LOAN",
"SELECT count(*) FROM m_loan_reschedule_request WHERE status_enum = 100"),
new ApprovalCounter("pending-account", "ALL_FUNCTIONS_READ",
"SELECT (SELECT count(*) FROM m_savings_account s LEFT JOIN m_client c ON c.id = s.client_id "
+ "LEFT JOIN m_group g ON g.id = s.group_id WHERE s.status_enum = 100 "
+ "AND COALESCE(s.terminate_true, false) = false AND " + CLIENT_OFFICE
+ ") + (SELECT count(*) FROM m_current_account ca WHERE ca.status_enum = 100 "
+ "AND COALESCE(ca.terminate_true, false) = false "
+ "AND EXISTS (SELECT 1 FROM m_office o WHERE o.id = ca.office_id AND o.hierarchy LIKE :h))"),
new ApprovalCounter("pending-share-accounts", "ALL_FUNCTIONS_READ",
"SELECT count(*) FROM m_share_account WHERE status_enum = 100"),
new ApprovalCounter("product-approval", "ALL_FUNCTIONS_READ",
"SELECT (SELECT count(*) FROM m_saving_product_temp WHERE status = 'PENDING' "
+ "AND product_type IN ('SAVINGS','CURRENT','SHARE','FD','RD')) + (SELECT count(*) " + MC_PENDING
+ "AND upper(aud.entity_name) IN (" + PRODUCT_ENTITIES + ") "
+ "AND NOT (upper(aud.entity_name) IN (" + STAGED_PRODUCT_ENTITIES + ") AND upper(aud.action_name) = 'UPDATE'))"),
new ApprovalCounter("pending-savings-account-edits", "ALL_FUNCTIONS_READ",
"SELECT (SELECT count(*) FROM m_savings_account_pending_edit WHERE status = 'PENDING') "
+ "+ (SELECT count(*) FROM m_current_account_pending_edit WHERE status = 'PENDING') "
+ "+ (SELECT count(*) FROM m_share_account_pending_edit WHERE status = 'PENDING')"),
new ApprovalCounter("pending-client-edits", "ALL_FUNCTIONS_READ",
"SELECT count(*) FROM m_client_pending_edit pe JOIN m_client c ON c.id = pe.client_id WHERE pe.status = 'PENDING'"),
new ApprovalCounter("pending-user-edits", "ALL_FUNCTIONS_READ",
"SELECT count(*) FROM m_user_pending_request WHERE status = 'PENDING'"),
// ── Requests raised by clerks, cashiers and managers ──────────────────────────
new ApprovalCounter("withdrawal-requests", "ALL_FUNCTIONS_READ",
"SELECT count(*) FROM m_clerk_withdrawal_request WHERE status = 'PENDING'"),
new ApprovalCounter("cash-transfers", "ALL_FUNCTIONS_READ",
"SELECT count(*) FROM m_cash_transfer_request WHERE status = 'PENDING'"),
new ApprovalCounter("voucher-corrections", "ALL_FUNCTIONS_READ",
"SELECT count(*) FROM m_voucher_correction WHERE status IN ('UNPASS_PENDING','CORRECTION_PENDING')"),
new ApprovalCounter("share-withdrawal-requests", "ALL_FUNCTIONS_READ",
"SELECT count(*) FROM m_clerk_share_withdrawal_request WHERE status = 'PENDING'"),
new ApprovalCounter("dividend-requests", "ALL_FUNCTIONS_READ",
"SELECT count(*) FROM m_share_product_dividend_pay_out p JOIN m_share_product sp ON sp.id = p.product_id "
+ "WHERE p.status = 100"),
new ApprovalCounter("share-addition-requests", "ALL_FUNCTIONS_READ",
"SELECT count(*) FROM m_share_account_transactions t JOIN m_share_account sa ON sa.id = t.account_id "
+ "JOIN m_client c ON c.id = sa.client_id JOIN m_share_product sp ON sp.id = sa.product_id "
+ "WHERE t.is_active = true AND t.status_enum = 100 AND t.type_enum = 500"),
new ApprovalCounter("deposit-requests", "ALL_FUNCTIONS_READ",
"SELECT count(*) FROM m_cashier_deposit_request WHERE status = 'PENDING'"),
new ApprovalCounter(DISBURSEMENT_REQUESTS, "ALL_FUNCTIONS_READ",
"SELECT count(*) FROM m_clerk_disbursement_request WHERE status = 'PENDING'"),
new ApprovalCounter("dds-approvals", null,
"SELECT count(*) FROM m_portfolio_command_source c JOIN m_permission p "
+ "ON p.code = c.action_name || '_' || c.entity_name AND p.can_maker_checker = true "
+ "WHERE c.status = 2 AND c.entity_name IN (" + DDS_ENTITIES + ")"),
new ApprovalCounter("chequebook-requests", "READ_CHEQUEBOOKPENDINGREQUEST",
"SELECT count(*) FROM m_chequebook_pending_request WHERE status = 'PENDING'"),
new ApprovalCounter("closure-requests", "ALL_FUNCTIONS_READ",
"SELECT count(*) FROM m_clerk_closure_request WHERE status = 'PENDING'"),
new ApprovalCounter("dd-issue-approvals", null,
"SELECT count(*) FROM m_sponsor_dd_issue_request WHERE status = 'PENDING'"),
new ApprovalCounter("dd-reprint-approvals", null,
"SELECT count(*) FROM m_sponsor_dd_reprint_request WHERE status = 'PENDING'"),
new ApprovalCounter("dd-revoke-approvals", null,
"SELECT count(*) FROM m_sponsor_dd_revoke_request WHERE status = 'PENDING'"),
new ApprovalCounter("risk-change-approvals", null,
"SELECT count(*) FROM m_client_risk_change_request WHERE status = 'PENDING'"));
}
@@ -1,24 +0,0 @@
package org.apache.fineract.portfolio.approvals.service;
import org.apache.fineract.portfolio.approvals.data.ApprovalCountsData;
import org.apache.fineract.portfolio.approvals.data.ApprovalCountsPlan;
import org.apache.fineract.useradministration.domain.AppUser;
/** Reads how many requests wait on each Approval Center tab, in one statement. */
public interface ApprovalCountsReadPlatformService {
/**
* Works out what the caller is counted for. Call it on the request thread: it reads the user's roles and the
* tenant of the current request.
*/
ApprovalCountsPlan planFor(AppUser user);
/** Counts now, for the tabs in the plan. The plan's tenant must be the current tenant of the calling thread. */
ApprovalCountsData retrieve(ApprovalCountsPlan plan);
/**
* Counts for the caller, from the answer held for a few seconds when there is one, so many screens asking together
* cost the database one query. {@code fresh} skips the held answer.
*/
ApprovalCountsData retrieveHeld(AppUser user, boolean fresh);
}
@@ -1,114 +0,0 @@
package org.apache.fineract.portfolio.approvals.service;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.portfolio.approvals.data.ApprovalCounter;
import org.apache.fineract.portfolio.approvals.data.ApprovalCountsData;
import org.apache.fineract.portfolio.approvals.data.ApprovalCountsPlan;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Service;
/**
* All the visible tabs' questions go to the database as one statement (one scalar sub-select per tab), so the cost is
* one connection and one plan instead of a call per tab. A tab the caller may not see is simply not asked.
*/
@Service
@RequiredArgsConstructor
public class ApprovalCountsReadPlatformServiceImpl implements ApprovalCountsReadPlatformService {
/** Screens asking together read one answer instead of each running the statement. */
private static final long HOLD_MILLIS = 3_000L;
private record Held(long at, ApprovalCountsData data) {}
private final JdbcTemplate jdbcTemplate;
private final Map<String, Held> held = new ConcurrentHashMap<>();
@Override
public ApprovalCountsPlan planFor(final AppUser user) {
final List<ApprovalCounter> visible = new ArrayList<>();
for (final ApprovalCounter counter : ApprovalCounterRegistry.ALL) {
if (isVisible(user, counter)) {
visible.add(counter);
}
}
final String hierarchy = user.getOffice().getHierarchy();
final boolean superChecker = !user.hasNotPermissionForAnyOf("ALL_FUNCTIONS", "CHECKER_SUPER_USER");
final MapSqlParameterSource params = new MapSqlParameterSource().addValue("h", hierarchy + "%")
.addValue("ho", ".".equals(hierarchy)).addValue("uid", user.getId()).addValue("superChecker", superChecker);
final FineractPlatformTenant tenant = ThreadLocalContextUtil.getTenant();
// Callers counted the same way share one query: only a limited checker's numbers depend on who they are.
final String group = tenant.getTenantIdentifier() + '|' + hierarchy + '|' + superChecker + '|'
+ (superChecker ? "" : user.getId()) + '|' + visible.stream().map(ApprovalCounter::key).collect(Collectors.joining(","));
return new ApprovalCountsPlan(tenant, visible, params, group);
}
@Override
public ApprovalCountsData retrieve(final ApprovalCountsPlan plan) {
final Map<String, Long> counts = new LinkedHashMap<>();
final List<ApprovalCounter> visible = plan.visible();
if (!visible.isEmpty()) {
final StringBuilder sql = new StringBuilder("SELECT ");
for (int i = 0; i < visible.size(); i++) {
sql.append(i == 0 ? "" : ", ").append('(').append(visible.get(i).sql()).append(") AS c").append(i);
}
final Map<String, Object> row = new NamedParameterJdbcTemplate(this.jdbcTemplate).queryForMap(sql.toString(),
plan.params());
for (int i = 0; i < visible.size(); i++) {
final long n = row.get("c" + i) instanceof Number number ? number.longValue() : 0L;
if (n > 0) {
counts.put(visible.get(i).key(), n);
}
}
}
return ApprovalCountsData.of(counts);
}
@Override
public ApprovalCountsData retrieveHeld(final AppUser user, final boolean fresh) {
final ApprovalCountsPlan plan = planFor(user);
final String holdKey = plan.tenant().getTenantIdentifier() + ':' + user.getId();
final long now = System.currentTimeMillis();
final Held recent = this.held.get(holdKey);
if (!fresh && recent != null && now - recent.at() < HOLD_MILLIS) {
return recent.data();
}
final ApprovalCountsData data = retrieve(plan);
this.held.put(holdKey, new Held(now, data));
if (this.held.size() > 500) {
this.held.entrySet().removeIf(e -> now - e.getValue().at() >= HOLD_MILLIS);
}
return data;
}
/** The tab's own visibility rule: its permission (READ_* is also met by ALL_FUNCTIONS_READ) and, for one tab, its role. */
private boolean isVisible(final AppUser user, final ApprovalCounter counter) {
if (counter.permission() != null) {
final boolean allowed = counter.permission().startsWith("READ_")
? user.hasAnyPermission(counter.permission(), "ALL_FUNCTIONS_READ")
: user.hasAnyPermission(counter.permission());
if (!allowed) {
return false;
}
}
return !ApprovalCounterRegistry.DISBURSEMENT_REQUESTS.equals(counter.key()) || isChecker(user);
}
private boolean isChecker(final AppUser user) {
if (user.hasAnyPermission("ALL_FUNCTIONS")) {
return true;
}
return user.getRoles().stream().map(r -> r.getName() == null ? "" : r.getName().toLowerCase().trim())
.anyMatch(n -> n.contains("manager") || n.contains("head cashier"));
}
}
@@ -1,145 +0,0 @@
package org.apache.fineract.portfolio.approvals.service;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.ws.rs.sse.OutboundSseEvent;
import jakarta.ws.rs.sse.Sse;
import jakarta.ws.rs.sse.SseEventSink;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.portfolio.approvals.data.ApprovalCountsData;
import org.apache.fineract.portfolio.approvals.data.ApprovalCountsPlan;
import org.springframework.stereotype.Service;
/**
* Keeps the Approvals badge live. A screen listens on a Server-Sent Events stream and is written to the moment its
* numbers change, so it neither polls nor reloads.
*
* One shared check, every {@value #CHECK_MILLIS} ms, serves every open screen, and screens counted the same way share
* its query. A comment line every {@value #PING_MILLIS} ms keeps proxies from closing an idle connection and tells the
* server when a screen has gone. A stream ends after {@value #LIFETIME_MILLIS} ms so the screen reconnects with a clean
* connection, which bounds what a browser accumulates on one.
*/
@Service
@RequiredArgsConstructor
public class ApprovalCountsStreamService {
/** How often the open screens are looked at: the longest a change waits before it is pushed. */
private static final long CHECK_MILLIS = 1_000L;
private static final long PING_MILLIS = 15_000L;
private static final long LIFETIME_MILLIS = 300_000L;
/** A screen listening, and the version of the numbers it was last sent. */
private static final class Listener {
private final ApprovalCountsPlan plan;
private final SseEventSink sink;
private final Sse sse;
private final long expiresAt;
private volatile String version;
private volatile long lastSent;
private Listener(final ApprovalCountsPlan plan, final SseEventSink sink, final Sse sse, final long expiresAt) {
this.plan = plan;
this.sink = sink;
this.sse = sse;
this.expiresAt = expiresAt;
}
}
private final ApprovalCountsReadPlatformService readService;
private final Set<Listener> listeners = ConcurrentHashMap.newKeySet();
private final ScheduledExecutorService ticker = Executors.newSingleThreadScheduledExecutor(runnable -> {
final Thread thread = new Thread(runnable, "approval-counts-stream");
thread.setDaemon(true);
return thread;
});
@PostConstruct
void startTicker() {
this.ticker.scheduleWithFixedDelay(this::tick, CHECK_MILLIS, CHECK_MILLIS, TimeUnit.MILLISECONDS);
}
@PreDestroy
void stopTicker() {
this.ticker.shutdownNow();
this.listeners.forEach(this::close);
}
/** Sends the numbers as they stand, then keeps the screen informed. Call it on the request thread. */
public void open(final ApprovalCountsPlan plan, final SseEventSink sink, final Sse sse) {
final Listener listener = new Listener(plan, sink, sse, System.currentTimeMillis() + LIFETIME_MILLIS);
try {
publish(listener, this.readService.retrieve(plan));
} catch (final RuntimeException e) {
sink.close();
throw e;
}
this.listeners.add(listener);
}
/** Looks at the numbers for every open screen and writes to the ones whose numbers moved. */
private void tick() {
if (this.listeners.isEmpty()) {
return;
}
final long now = System.currentTimeMillis();
final Map<String, ApprovalCountsData> computed = new HashMap<>();
for (final Listener listener : this.listeners) {
try {
if (listener.sink.isClosed() || now >= listener.expiresAt) {
close(listener);
continue;
}
ApprovalCountsData data = computed.get(listener.plan.group());
if (data == null) {
ThreadLocalContextUtil.setTenant(listener.plan.tenant());
data = this.readService.retrieve(listener.plan);
computed.put(listener.plan.group(), data);
}
if (!data.version().equals(listener.version)) {
publish(listener, data);
} else if (now - listener.lastSent >= PING_MILLIS) {
listener.lastSent = now;
send(listener, listener.sse.newEventBuilder().comment("ping").build());
}
} catch (final RuntimeException e) {
close(listener);
} finally {
ThreadLocalContextUtil.clearTenant();
}
}
}
private void publish(final Listener listener, final ApprovalCountsData data) {
listener.version = data.version();
listener.lastSent = System.currentTimeMillis();
send(listener, listener.sse.newEvent("counts", data.toJson()));
}
private void send(final Listener listener, final OutboundSseEvent event) {
listener.sink.send(event).whenComplete((sent, failure) -> {
if (failure != null) {
close(listener);
}
});
}
private void close(final Listener listener) {
this.listeners.remove(listener);
try {
listener.sink.close();
} catch (final RuntimeException ignored) {
// The screen is already gone.
}
}
}
@@ -46,6 +46,8 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.cashreport.service.VaultPositionService; import org.apache.fineract.portfolio.cashreport.service.VaultPositionService;
import org.apache.fineract.portfolio.denominationmaster.service.CountedCashLedger; 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.SavingsDailyInterestOrchestrator;
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsInterestPostingReportService; import org.apache.fineract.portfolio.savings.dailyinterest.SavingsInterestPostingReportService;
import org.apache.fineract.portfolio.savings.fdinterest.FixedDepositInterestPostingOrchestrator; import org.apache.fineract.portfolio.savings.fdinterest.FixedDepositInterestPostingOrchestrator;
@@ -95,8 +97,16 @@ public class BranchDayRegisterApiResource {
private final SavingsDailyInterestOrchestrator savingsDailyInterestOrchestrator; private final SavingsDailyInterestOrchestrator savingsDailyInterestOrchestrator;
private final FixedDepositInterestPostingReportService fdInterestPostingReportService; private final FixedDepositInterestPostingReportService fdInterestPostingReportService;
private final FixedDepositInterestPostingOrchestrator fdInterestPostingOrchestrator; private final FixedDepositInterestPostingOrchestrator fdInterestPostingOrchestrator;
private final InvestmentPurchaseReadPlatformService investmentPurchaseReadPlatformService;
private final InvestmentFdInterestService investmentFdInterestService;
private final Gson gson = new Gson(); 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 ────────────────────────────── // ── GET /v1/branchday ── the register itself ──────────────────────────────
/** /**
@@ -260,6 +270,13 @@ public class BranchDayRegisterApiResource {
final Map<String, Object> fdInterest = fdInterestPostingReportService.dayCloseInterestView(officeId, businessDate); final Map<String, Object> fdInterest = fdInterestPostingReportService.dayCloseInterestView(officeId, businessDate);
result.put("fdInterestPending", fdInterest.get("pending")); result.put("fdInterestPending", fdInterest.get("pending"));
result.put("fdInterestDue", fdInterest.get("due")); 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); return gson.toJson(result);
} }
@@ -56,7 +56,6 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.domain.KycExpiryStage; import org.apache.fineract.portfolio.client.domain.KycExpiryStage;
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard; import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
import org.apache.fineract.portfolio.savings.service.FixedDepositFundingValidator;
import org.apache.fineract.portfolio.voucher.service.VoucherLine; import org.apache.fineract.portfolio.voucher.service.VoucherLine;
import org.apache.fineract.portfolio.voucher.service.VoucherService; import org.apache.fineract.portfolio.voucher.service.VoucherService;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -99,7 +98,6 @@ public class CashierDepositRequestApiResource {
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final VoucherService voucherService; private final VoucherService voucherService;
private final FixedDepositFundingValidator fixedDepositFundingValidator;
private final Gson gson = new GsonBuilder().serializeNulls().create(); private final Gson gson = new GsonBuilder().serializeNulls().create();
// ── POST /v1/cashier/deposit-requests ── Cashier submits new deposit request ── // ── POST /v1/cashier/deposit-requests ── Cashier submits new deposit request ──
@@ -157,11 +155,6 @@ public class CashierDepositRequestApiResource {
throw apiError(400, "Amount must be greater than zero"); throw apiError(400, "Amount must be greater than zero");
} }
kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.CREDIT); kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.CREDIT);
// An FD is funded once, with exactly its principal. Told here at the counter rather than when the
// manager approves; the FD deposit command checks the same again at posting.
if (!isGlType && "FIXED_DEPOSIT".equals(resolveDepositAccountCategory(savingsAccountId, accountNo))) {
fixedDepositFundingValidator.validateFirstFunding(savingsAccountId, amount);
}
validateDenominationTotal(denominationData, amount); validateDenominationTotal(denominationData, amount);
validateDdLimit(ddFaceValue); validateDdLimit(ddFaceValue);
@@ -20,7 +20,6 @@ import org.apache.fineract.portfolio.cashreport.service.CashierRoleHelper;
import org.apache.fineract.portfolio.cashreport.service.VaultPositionService; import org.apache.fineract.portfolio.cashreport.service.VaultPositionService;
import org.apache.fineract.portfolio.denominationmaster.service.CountedCashLedger; import org.apache.fineract.portfolio.denominationmaster.service.CountedCashLedger;
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter; import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
import org.apache.fineract.portfolio.voucher.service.VoucherService;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.DateUtils;
@@ -37,7 +36,6 @@ public class CashReportApiResource {
private final DenominationBreakdownWriter denominationBreakdownWriter; private final DenominationBreakdownWriter denominationBreakdownWriter;
private final CashierRoleHelper cashierRoleHelper; private final CashierRoleHelper cashierRoleHelper;
private final VaultPositionService vaultPositionService; private final VaultPositionService vaultPositionService;
private final VoucherService voucherService;
private final Gson gson = new Gson(); private final Gson gson = new Gson();
/** /**
@@ -331,18 +329,7 @@ public class CashReportApiResource {
BigDecimal priorCreditTotal = BigDecimal.ZERO; BigDecimal priorCreditTotal = BigDecimal.ZERO;
BigDecimal priorDebitTotal = BigDecimal.ZERO; BigDecimal priorDebitTotal = BigDecimal.ZERO;
// Where each voucher stands after a correction: a revoked voucher is listed, marked, but its money now sits on
// the voucher that replaced it, so it must not be counted in the day's totals a second time.
final Map<String, Map<String, Object>> marks = voucherService.voucherMarks(
rows.stream().map(r -> (String) r.get("voucherNo")).toList());
for (final Map<String, Object> row : rows) { for (final Map<String, Object> row : rows) {
final Map<String, Object> mark = marks.get((String) row.get("voucherNo"));
row.put("voucherStatus", mark == null ? "ACTIVE" : mark.get("voucherStatus"));
row.put("revokedByVoucherNo", mark == null ? null : mark.get("revokedByVoucherNo"));
row.put("replacesVoucherNo", mark == null ? null : mark.get("replacesVoucherNo"));
row.put("correctionNote", mark == null ? null : mark.get("correctionNote"));
final boolean revoked = "REVOKED".equals(row.get("voucherStatus"));
if (sideFilter != null && !sideFilter.equals(row.get("side"))) { if (sideFilter != null && !sideFilter.equals(row.get("side"))) {
continue; continue;
} }
@@ -363,7 +350,7 @@ public class CashReportApiResource {
// was already earned, and the day it belongs to has been counted and signed off. Adding // was already earned, and the day it belongs to has been counted and signed off. Adding
// it to this day's totals would make them disagree with the ledger for either date. // it to this day's totals would make them disagree with the ledger for either date.
final boolean priorDay = "SYSTEM_PRIOR".equals(row.get("section")); final boolean priorDay = "SYSTEM_PRIOR".equals(row.get("section"));
if (!priorDay && !revoked && ("APPROVED".equalsIgnoreCase(rowStatus) || "COMPLETED".equalsIgnoreCase(rowStatus) if (!priorDay && ("APPROVED".equalsIgnoreCase(rowStatus) || "COMPLETED".equalsIgnoreCase(rowStatus)
|| "WITHDRAWN".equalsIgnoreCase(rowStatus) || "CLOSED".equalsIgnoreCase(rowStatus))) { || "WITHDRAWN".equalsIgnoreCase(rowStatus) || "CLOSED".equalsIgnoreCase(rowStatus))) {
final BigDecimal amount = (BigDecimal) row.get("amount"); final BigDecimal amount = (BigDecimal) row.get("amount");
if (amount != null) { if (amount != null) {
@@ -57,23 +57,9 @@ public class CashTransferApiResource {
private final VaultPositionService vaultPositionService; private final VaultPositionService vaultPositionService;
private final Gson gson = new Gson(); private final Gson gson = new Gson();
/**
* Cash no longer leaves the vault on one person's say-so: the allocation is raised as a request (see
* {@link #submitRequest}) and only a different person's approval posts it. The posting itself is
* {@link #postTransfer}.
*/
@POST @POST
public String create(String body) { public String create(String body) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.required", this.context.authenticatedUser().validateHasPermissionTo("CREATE_CASHTRANSFER");
"A cash transfer is raised as a request and posted when a manager approves it.");
}
/**
* Validates a transfer and, unless {@code dryRun}, posts it: the header and its denominations, then the GL entry.
* A dry run stops after every check has passed, which is what a request is held to when it is raised and again
* when it is approved, since the vault can change in between.
*/
private String postTransfer(final String body, final Long performedBy, final String performedByUsername, final boolean dryRun) {
JsonObject json = JsonParser.parseString(body).getAsJsonObject(); JsonObject json = JsonParser.parseString(body).getAsJsonObject();
final LocalDate transferDate = json.has("transferDate") && !json.get("transferDate").isJsonNull() final LocalDate transferDate = json.has("transferDate") && !json.get("transferDate").isJsonNull()
@@ -116,9 +102,6 @@ public class CashTransferApiResource {
} }
validateVaultHasSufficientCash(fromTellerId, total, denominations, transferDate); validateVaultHasSufficientCash(fromTellerId, total, denominations, transferDate);
if (dryRun) {
return "{}";
}
CashTransfer header = new CashTransfer(); CashTransfer header = new CashTransfer();
header.setTransferDate(transferDate); header.setTransferDate(transferDate);
@@ -129,9 +112,15 @@ public class CashTransferApiResource {
header.setTransferAmount(total); header.setTransferAmount(total);
header.setNote(note); header.setNote(note);
header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant())); header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
// Recorded against the person who raised the request, not the one who approved it. try {
header.setPerformedBy(performedBy); AppUser user = this.context.authenticatedUser();
header.setPerformedByUsername(performedByUsername); if (user != null) {
header.setPerformedBy(user.getId());
header.setPerformedByUsername(user.getUsername());
}
} catch (Exception e) {
// resolving the current user must never block recording the transfer
}
// Save header + details in one committed transaction, then GL dispatch separately. // Save header + details in one committed transaction, then GL dispatch separately.
// This prevents EclipseLink from flushing pending detail rows inside Fineract's // This prevents EclipseLink from flushing pending detail rows inside Fineract's
@@ -170,171 +159,6 @@ public class CashTransferApiResource {
return gson.toJson(result); return gson.toJson(result);
} }
// ── Maker-checker: the allocation is a request until a different person approves it ────────────
/** The maker raises the allocation. Nothing moves yet: every check runs, and the request waits for a checker. */
@POST
@Path("requests")
public String submitRequest(final String body) {
final AppUser user = this.context.authenticatedUser();
user.validateHasPermissionTo("CREATE_CASHTRANSFER");
postTransfer(body, user.getId(), user.getUsername(), true);
// The same request, from the same person, is not raised twice while the first is still waiting: a second tap or
// a second click would otherwise release the same cash twice once both were approved.
final Integer identical = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM m_cash_transfer_request WHERE status = ? AND maker_id = ? AND request_body = ?", Integer.class,
REQUEST_PENDING, user.getId(), body);
if (identical != null && identical > 0) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.duplicate",
"An identical cash transfer request from you is already waiting for approval.");
}
final JsonObject json = JsonParser.parseString(body).getAsJsonObject();
final JsonArray denominations = json.getAsJsonArray("denominations");
final Long id = jdbcTemplate.queryForObject("""
INSERT INTO m_cash_transfer_request
(transfer_date, from_teller_id, to_cashier_id, from_party, to_party, note, total_amount, request_body,
status, maker_id, maker_name, maker_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
RETURNING id
""", Long.class, textOrNull(json, "transferDate") == null ? null : java.sql.Date.valueOf(textOrNull(json, "transferDate")),
json.get("fromTellerId").getAsLong(), json.get("toCashierId").getAsLong(), textOrNull(json, "fromParty"),
textOrNull(json, "toParty"), textOrNull(json, "note"), sumTotal(denominations), body, REQUEST_PENDING, user.getId(),
user.getDisplayName(), java.sql.Timestamp.from(java.time.Instant.now()));
final JsonObject result = new JsonObject();
result.addProperty("resourceId", id);
result.addProperty("status", REQUEST_PENDING);
return gson.toJson(result);
}
/** Requests by status (comma separated); {@code mine} narrows them to the ones the caller raised. */
@GET
@Path("requests")
public String listRequests(@QueryParam("status") final String status,
@QueryParam("mine") @jakarta.ws.rs.DefaultValue("false") final boolean mine) {
final AppUser user = this.context.authenticatedUser();
final StringBuilder sql = new StringBuilder("SELECT * FROM m_cash_transfer_request WHERE 1=1");
final List<Object> params = new java.util.ArrayList<>();
if (status != null && !status.isBlank()) {
final String[] statuses = status.split(",");
sql.append(" AND status IN (").append("?,".repeat(statuses.length), 0, statuses.length * 2 - 1).append(")");
for (final String s : statuses) {
params.add(s.trim().toUpperCase());
}
}
if (mine) {
sql.append(" AND maker_id = ?");
params.add(user.getId());
}
sql.append(" ORDER BY id DESC LIMIT 200");
final List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql.toString(), params.toArray());
for (final Map<String, Object> row : rows) {
// The notes asked for, so the approver sees what they are releasing without opening anything.
final Object requestBody = row.remove("request_body");
row.put("denominations", requestBody == null ? new JsonArray()
: JsonParser.parseString(requestBody.toString()).getAsJsonObject().getAsJsonArray("denominations"));
}
return gson.toJson(rows);
}
/** The checker releases the cash: re-checks the vault, then posts the transfer exactly as a direct one used to. */
@POST
@Path("requests/{id}/approve")
public String approveRequest(@PathParam("id") final Long id) {
final AppUser user = this.context.authenticatedUser();
requireChecker(user);
final Map<String, Object> r = loadRequest(id);
requirePending(r, "approved");
final Long makerId = r.get("maker_id") instanceof Number n ? n.longValue() : null;
if (makerId != null && makerId.equals(user.getId())) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.same.user",
"Maker-checker: you cannot approve a cash transfer you raised yourself. Ask another manager.");
}
final String posted = postTransfer((String) r.get("request_body"), makerId, (String) r.get("maker_name"), false);
final JsonObject postedJson = JsonParser.parseString(posted).getAsJsonObject();
final Long transferId = postedJson.has("resourceId") ? postedJson.get("resourceId").getAsLong() : null;
final int updated = jdbcTemplate.update("""
UPDATE m_cash_transfer_request SET status = ?, checker_id = ?, checker_name = ?, checker_at = ?, cash_transfer_id = ?
WHERE id = ? AND status = ?
""", REQUEST_APPROVED, user.getId(), user.getDisplayName(), java.sql.Timestamp.from(java.time.Instant.now()),
transferId, id, REQUEST_PENDING);
if (updated == 0) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.actioned",
"This request was already actioned by another manager.");
}
final JsonObject result = new JsonObject();
result.addProperty("resourceId", id);
result.addProperty("status", REQUEST_APPROVED);
if (transferId != null) {
result.addProperty("cashTransferId", transferId);
}
if (postedJson.has("glWarning")) {
result.add("glWarning", postedJson.get("glWarning"));
}
return gson.toJson(result);
}
@POST
@Path("requests/{id}/reject")
public String rejectRequest(@PathParam("id") final Long id, final String body) {
final AppUser user = this.context.authenticatedUser();
requireChecker(user);
final Map<String, Object> r = loadRequest(id);
requirePending(r, "rejected");
final JsonObject json = body == null || body.isBlank() ? new JsonObject() : JsonParser.parseString(body).getAsJsonObject();
final String reason = textOrNull(json, "reason");
if (reason == null || reason.isBlank()) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.reason", "A reason is required to reject.");
}
jdbcTemplate.update("""
UPDATE m_cash_transfer_request SET status = ?, checker_id = ?, checker_name = ?, checker_at = ?, rejection_reason = ?
WHERE id = ? AND status = ?
""", REQUEST_REJECTED, user.getId(), user.getDisplayName(), java.sql.Timestamp.from(java.time.Instant.now()),
reason.trim(), id, REQUEST_PENDING);
final JsonObject result = new JsonObject();
result.addProperty("resourceId", id);
result.addProperty("status", REQUEST_REJECTED);
return gson.toJson(result);
}
private static final String REQUEST_PENDING = "PENDING";
private static final String REQUEST_APPROVED = "APPROVED";
private static final String REQUEST_REJECTED = "REJECTED";
private Map<String, Object> loadRequest(final Long id) {
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("SELECT * FROM m_cash_transfer_request WHERE id = ?", id);
if (rows.isEmpty()) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.not.found", "Cash transfer request " + id + " not found.");
}
return rows.get(0);
}
private void requirePending(final Map<String, Object> request, final String action) {
if (!REQUEST_PENDING.equals(request.get("status"))) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.status",
"Only PENDING requests can be " + action + ". Current status: " + request.get("status"));
}
}
/** Checker authorization, the same rule the withdrawal and deposit request registers use. */
private void requireChecker(final AppUser user) {
if (user.hasAnyPermission("ALL_FUNCTIONS")) {
return;
}
final boolean isChecker = user.getRoles().stream().map(role -> role.getName() == null ? "" : role.getName().toLowerCase().trim())
.anyMatch(n -> n.contains("manager") || n.contains("head cashier"));
if (!isChecker) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.checker",
"Only a Bank Manager (or Head Cashier) can approve or reject a cash transfer.");
}
}
private static String textOrNull(final JsonObject json, final String key) {
return json.has(key) && !json.get(key).isJsonNull() ? json.get(key).getAsString() : null;
}
@GET @GET
public String getAll( public String getAll(
@QueryParam("offset") @jakarta.ws.rs.DefaultValue("0") int offset, @QueryParam("offset") @jakarta.ws.rs.DefaultValue("0") int offset,
@@ -69,8 +69,6 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte
import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.PortfolioProductType;
import org.apache.fineract.portfolio.voucher.service.VoucherLine; import org.apache.fineract.portfolio.voucher.service.VoucherLine;
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard; import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService;
import org.apache.fineract.portfolio.savings.service.FixedDepositFundingValidator;
import org.apache.fineract.portfolio.voucher.service.VoucherService; import org.apache.fineract.portfolio.voucher.service.VoucherService;
import org.apache.fineract.useradministration.domain.AppUser; import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
@@ -92,8 +90,6 @@ public class ClerkDisbursementRequestApiResource {
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final FineractRequestContextHolder fineractRequestContextHolder; private final FineractRequestContextHolder fineractRequestContextHolder;
private final VoucherService voucherService; private final VoucherService voucherService;
private final FixedDepositFundingValidator fixedDepositFundingValidator;
private final DepositAccountWritePlatformService depositAccountWritePlatformService;
private final Gson gson = new GsonBuilder().serializeNulls().create(); private final Gson gson = new GsonBuilder().serializeNulls().create();
// ── POST /v1/clerk/disbursement-requests ── Clerk submits new request ──────── // ── POST /v1/clerk/disbursement-requests ── Clerk submits new request ────────
@@ -206,9 +202,6 @@ public class ClerkDisbursementRequestApiResource {
} }
} }
// Fixed deposit legs: credit-only, funded once with exactly the principal, from the customer's own account.
validateFixedDepositLegs(creditEntries, requestType);
// Validate balance when both DR and CR entries present (collection mode) // Validate balance when both DR and CR entries present (collection mode)
if (totalDr.compareTo(BigDecimal.ZERO) > 0 && totalCr.compareTo(BigDecimal.ZERO) > 0 if (totalDr.compareTo(BigDecimal.ZERO) > 0 && totalCr.compareTo(BigDecimal.ZERO) > 0
&& totalDr.subtract(totalCr).abs().compareTo(new BigDecimal("0.01")) > 0) { && totalDr.subtract(totalCr).abs().compareTo(new BigDecimal("0.01")) > 0) {
@@ -498,10 +491,6 @@ public class ClerkDisbursementRequestApiResource {
rejectRecurringDepositLeg(entry, entryType); rejectRecurringDepositLeg(entry, entryType);
} }
} }
// Re-checked here, not only at submit: another request (or a counter deposit) may have funded the
// same FD while this one waited, and a refusal now still leaves the request PENDING.
final Set<Long> fundedFixedDeposits = validateFixedDepositLegs(creditEntries,
record.get("request_type") != null ? (String) record.get("request_type") : "DISBURSEMENT");
// Atomic PENDING → PROCESSING lock: prevents two managers from concurrently // Atomic PENDING → PROCESSING lock: prevents two managers from concurrently
// posting duplicate Fineract transactions for the same request. // posting duplicate Fineract transactions for the same request.
@@ -784,12 +773,6 @@ public class ClerkDisbursementRequestApiResource {
} }
} // end else (disbursement/collection mode) } // end else (disbursement/collection mode)
// A funded FD's maturity date and amount were projected off its principal with no money in it;
// the direct credit above does not recompute them, so do it now that the money has landed.
for (final Long fdId : fundedFixedDeposits) {
depositAccountWritePlatformService.recalculateFixedDepositMaturity(fdId);
}
} catch (Exception ex) { } catch (Exception ex) {
// Compensate: undo all posted Fineract transactions in reverse order (best-effort). // Compensate: undo all posted Fineract transactions in reverse order (best-effort).
// This restores account balances even when a later entry fails mid-processing. // This restores account balances even when a later entry fails mid-processing.
@@ -1240,70 +1223,6 @@ public class ClerkDisbursementRequestApiResource {
} }
} }
/**
* Fixed deposit legs of a clerk request, recognised by id (deposit_type_enum) rather than the entry's label.
* <ul>
* <li>An FD is never debited here: money leaves an FD only through its maturity or premature closure. The
* direct book debit used by Bank Transfer and Collection would otherwise bypass the FD's own refusal.</li>
* <li>An FD credit is its first and only funding, for exactly its principal, and an FD appears once.</li>
* <li>The FDs funded by one request belong to one customer, and every Savings/Current account debited to fund
* them belongs to that customer (or is held jointly) and can pay its amount. A GL source (Disbursement) is
* the bank's own money and has no customer to compare.</li>
* </ul>
*
* @return the ids of the FDs this request funds, for the maturity recalculation after posting
*/
private Set<Long> validateFixedDepositLegs(final JsonArray entries, final String requestType) {
final Set<Long> fdIds = new LinkedHashSet<>();
Long firstFdId = null;
Long fdClientId = null;
for (final JsonElement el : entries) {
final JsonObject entry = el.getAsJsonObject();
final String type = entry.has("accountType") ? entry.get("accountType").getAsString() : "SAVINGS";
if ("GL".equals(type) || "CURRENT".equals(type) || !entry.has("savingsAccountId")
|| entry.get("savingsAccountId").isJsonNull()) {
continue;
}
final Long accountId = entry.get("savingsAccountId").getAsLong();
if (!fixedDepositFundingValidator.isFixedDeposit(accountId)) {
continue;
}
final String acNo = entry.has("accountNo") ? entry.get("accountNo").getAsString() : String.valueOf(accountId);
if ("Dr".equals(entry.has("dbCr") ? entry.get("dbCr").getAsString() : "Cr")) {
throw apiError(422, "Fixed deposit " + acNo + " cannot be debited. Money leaves a fixed deposit only"
+ " through its maturity or premature closure.");
}
if (!fdIds.add(accountId)) {
throw apiError(422, "Fixed deposit " + acNo + " appears more than once. A fixed deposit is funded once,"
+ " with its full principal in one entry.");
}
final BigDecimal amount = parseBigDecimal(entry.has("amount") ? entry.get("amount").getAsString() : null);
final Long clientId = fixedDepositFundingValidator.validateFirstFunding(accountId, amount);
if (firstFdId == null) {
firstFdId = accountId;
fdClientId = clientId;
} else if (!Objects.equals(fdClientId, clientId)) {
throw apiError(422, "One " + voucherTypeLabelFor(requestType) + " can fund fixed deposits of one customer"
+ " only. Raise a separate request for each customer.");
}
}
if (fdIds.isEmpty()) {
return fdIds;
}
for (final JsonElement el : entries) {
final JsonObject entry = el.getAsJsonObject();
final String type = entry.has("accountType") ? entry.get("accountType").getAsString() : "SAVINGS";
if (!"Dr".equals(entry.has("dbCr") ? entry.get("dbCr").getAsString() : "Cr") || "GL".equals(type)
|| !entry.has("savingsAccountId") || entry.get("savingsAccountId").isJsonNull()) {
continue;
}
final BigDecimal amount = parseBigDecimal(entry.has("amount") ? entry.get("amount").getAsString() : null);
fixedDepositFundingValidator.validateFundingSource(firstFdId, fdClientId, entry.get("savingsAccountId").getAsLong(),
"CURRENT".equals(type), amount);
}
return fdIds;
}
/** Savings status_enum for an account still open for business (Fineract's SavingsAccountStatusType.ACTIVE). */ /** Savings status_enum for an account still open for business (Fineract's SavingsAccountStatusType.ACTIVE). */
private static final int SAVINGS_STATUS_ACTIVE = 300; private static final int SAVINGS_STATUS_ACTIVE = 300;
@@ -48,7 +48,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
@@ -57,7 +56,6 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.domain.KycExpiryStage; import org.apache.fineract.portfolio.client.domain.KycExpiryStage;
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard; import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
import org.apache.fineract.portfolio.savings.service.FixedDepositFundingValidator;
import org.apache.fineract.portfolio.voucher.service.VoucherLine; import org.apache.fineract.portfolio.voucher.service.VoucherLine;
import org.apache.fineract.portfolio.voucher.service.VoucherService; import org.apache.fineract.portfolio.voucher.service.VoucherService;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -86,7 +84,6 @@ public class ClerkWithdrawalRequestApiResource {
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final VoucherService voucherService; private final VoucherService voucherService;
private final FixedDepositFundingValidator fixedDepositFundingValidator;
private final Gson gson = new GsonBuilder().serializeNulls().create(); private final Gson gson = new GsonBuilder().serializeNulls().create();
// ── POST /v1/clerk/withdrawal-requests ── Clerk submits new request ───────── // ── POST /v1/clerk/withdrawal-requests ── Clerk submits new request ─────────
@@ -113,28 +110,13 @@ public class ClerkWithdrawalRequestApiResource {
final String tokNo = fromJsonHelper.extractStringNamed("tokNo", root); final String tokNo = fromJsonHelper.extractStringNamed("tokNo", root);
final String txnDate = fromJsonHelper.extractStringNamed("txnDate", root); final String txnDate = fromJsonHelper.extractStringNamed("txnDate", root);
final String denominationData = fromJsonHelper.extractStringNamed("denominationData", root); final String denominationData = fromJsonHelper.extractStringNamed("denominationData", root);
// A GL withdrawal slip (e.g. Sundry Creditors paid out in cash) carries the GL and the cash GL it is
// paid from instead of a savings account; it is approved and disbursed exactly like any other slip.
final boolean isGl = "GL".equalsIgnoreCase(accountType);
final Long glAccountId = fromJsonHelper.extractLongNamed("glAccountId", root);
final Long contraGlId = fromJsonHelper.extractLongNamed("contraGlId", root);
final Long officeId = fromJsonHelper.extractLongNamed("officeId", root);
final String currencyCode = fromJsonHelper.extractStringNamed("currencyCode", root);
if (accountNo == null || accountNo.isBlank()) { if (accountNo == null || accountNo.isBlank()) {
throw apiError(400, "Account number is required"); throw apiError(400, "Account number is required");
} }
if (isGl) { if (savingsAccountId == null) {
validateGlWithdrawalAccounts(glAccountId, contraGlId);
} else if (savingsAccountId == null) {
throw apiError(400, "Savings account id is required for a withdrawal request"); throw apiError(400, "Savings account id is required for a withdrawal request");
} }
// Money leaves a fixed deposit only through its maturity or premature closure, never a withdrawal slip.
// Checked by id: the slip's own label is not trusted, and a CURRENT id belongs to m_current_account.
if (!isGl && !"CURRENT".equalsIgnoreCase(accountType) && fixedDepositFundingValidator.isFixedDeposit(savingsAccountId)) {
throw apiError(422, "Account " + accountNo + " is a fixed deposit and cannot be withdrawn from."
+ " Money leaves a fixed deposit only through its maturity or premature closure.");
}
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw apiError(400, "Amount must be greater than zero"); throw apiError(400, "Amount must be greater than zero");
} }
@@ -142,9 +124,7 @@ public class ClerkWithdrawalRequestApiResource {
throw apiError(400, "Token number (TOK. NO.) is required"); throw apiError(400, "Token number (TOK. NO.) is required");
} }
validateTokenUniqueToday(tokNo, null); validateTokenUniqueToday(tokNo, null);
if (!isGl) {
kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.DEBIT); kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.DEBIT);
}
final String activityLog = buildActivityLog(null, "SUBMITTED", currentUser.getUsername(), null); final String activityLog = buildActivityLog(null, "SUBMITTED", currentUser.getUsername(), null);
@@ -152,9 +132,8 @@ public class ClerkWithdrawalRequestApiResource {
INSERT INTO m_clerk_withdrawal_request INSERT INTO m_clerk_withdrawal_request
(account_no, account_type, savings_account_id, client_id, client_name, amount, (account_no, account_type, savings_account_id, client_id, client_name, amount,
payment_type_id, payment_type_name, description, cheque_no, bank_name, tok_no, txn_date, payment_type_id, payment_type_name, description, cheque_no, bank_name, tok_no, txn_date,
status, clerk_id, clerk_name, clerk_username, denomination_data, activity_log, submitted_at, status, clerk_id, clerk_name, clerk_username, denomination_data, activity_log, submitted_at)
gl_account_id, contra_gl_id, office_id, currency_code) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
RETURNING id RETURNING id
""", """,
Long.class, Long.class,
@@ -163,9 +142,7 @@ public class ClerkWithdrawalRequestApiResource {
txnDate != null ? Date.valueOf(txnDate) : null, txnDate != null ? Date.valueOf(txnDate) : null,
"PENDING", "PENDING",
currentUser.getId(), currentUser.getDisplayName(), currentUser.getUsername(), currentUser.getId(), currentUser.getDisplayName(), currentUser.getUsername(),
denominationData, activityLog, Timestamp.from(Instant.now()), denominationData, activityLog, Timestamp.from(Instant.now())
isGl ? glAccountId : null, isGl ? contraGlId : null, isGl ? (officeId != null ? officeId : 1L) : null,
isGl ? (currencyCode != null ? currencyCode : "INR") : null
); );
// Globally unique voucher number from the central m_voucher register // Globally unique voucher number from the central m_voucher register
@@ -221,8 +198,7 @@ public class ClerkWithdrawalRequestApiResource {
+ " txn_date, status, clerk_id, clerk_name, clerk_username," + " txn_date, status, clerk_id, clerk_name, clerk_username,"
+ " manager_id, manager_name, manager_username, rejection_reason, txn_id," + " manager_id, manager_name, manager_username, rejection_reason, txn_id,"
+ " denomination_data, activity_log, submitted_at, actioned_at," + " denomination_data, activity_log, submitted_at, actioned_at,"
+ " cashier_id, disbursed_by, disbursed_at, voucher_no," + " cashier_id, disbursed_by, disbursed_at, voucher_no"
+ " gl_account_id, contra_gl_id, office_id, currency_code"
+ " FROM m_clerk_withdrawal_request " + where + " FROM m_clerk_withdrawal_request " + where
+ " ORDER BY submitted_at DESC LIMIT ? OFFSET ?", + " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
pageParams.toArray()); pageParams.toArray());
@@ -276,8 +252,7 @@ public class ClerkWithdrawalRequestApiResource {
payment_type_name, description, tok_no, txn_date, status, payment_type_name, description, tok_no, txn_date, status,
clerk_name, manager_name, manager_username, rejection_reason, txn_id, clerk_name, manager_name, manager_username, rejection_reason, txn_id,
submitted_at, actioned_at, submitted_at, actioned_at,
cashier_id, disbursed_by, disbursed_at, voucher_no, cashier_id, disbursed_by, disbursed_at, voucher_no
gl_account_id, contra_gl_id, office_id, currency_code
FROM m_clerk_withdrawal_request FROM m_clerk_withdrawal_request
""" + where + " ORDER BY submitted_at DESC LIMIT ? OFFSET ?", """ + where + " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
pageParams.toArray() pageParams.toArray()
@@ -301,8 +276,7 @@ public class ClerkWithdrawalRequestApiResource {
txn_date, status, clerk_id, clerk_name, clerk_username, txn_date, status, clerk_id, clerk_name, clerk_username,
manager_id, manager_name, manager_username, rejection_reason, txn_id, manager_id, manager_name, manager_username, rejection_reason, txn_id,
denomination_data, activity_log, submitted_at, actioned_at, denomination_data, activity_log, submitted_at, actioned_at,
cashier_id, disbursed_by, disbursed_at, voucher_no, cashier_id, disbursed_by, disbursed_at, voucher_no
gl_account_id, contra_gl_id, office_id, currency_code
FROM m_clerk_withdrawal_request WHERE id = ? FROM m_clerk_withdrawal_request WHERE id = ?
""", id); """, id);
if (rows.isEmpty()) { if (rows.isEmpty()) {
@@ -321,8 +295,7 @@ public class ClerkWithdrawalRequestApiResource {
final List<Map<String, Object>> rows = jdbcTemplate.queryForList(""" final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
SELECT id, account_no, account_type, savings_account_id, client_id, client_name, SELECT id, account_no, account_type, savings_account_id, client_id, client_name,
amount, payment_type_id, payment_type_name, description, tok_no, status, amount, payment_type_id, payment_type_name, description, tok_no, status,
clerk_id, clerk_name, manager_id, manager_name, submitted_at, actioned_at, voucher_no, clerk_id, clerk_name, manager_id, manager_name, submitted_at, actioned_at, voucher_no
gl_account_id, contra_gl_id, office_id, currency_code
FROM m_clerk_withdrawal_request FROM m_clerk_withdrawal_request
WHERE tok_no = ? AND CAST(submitted_at AS DATE) = CURRENT_DATE WHERE tok_no = ? AND CAST(submitted_at AS DATE) = CURRENT_DATE
ORDER BY submitted_at DESC LIMIT 1 ORDER BY submitted_at DESC LIMIT 1
@@ -482,16 +455,13 @@ public class ClerkWithdrawalRequestApiResource {
"Maker-checker: you cannot approve a withdrawal request you submitted yourself. Ask another manager to review it."); "Maker-checker: you cannot approve a withdrawal request you submitted yourself. Ask another manager to review it.");
} }
final boolean isGlRequest = "GL".equalsIgnoreCase((String) record.get("account_type"));
final Long savingsAccountId = record.get("savings_account_id") != null final Long savingsAccountId = record.get("savings_account_id") != null
? ((Number) record.get("savings_account_id")).longValue() : null; ? ((Number) record.get("savings_account_id")).longValue() : null;
// Checked again on approval, not only on submission: a request can sit in the queue for days, // Checked again on approval, not only on submission: a request can sit in the queue for days,
// and the account can cross the dormancy threshold while it waits. The savings withdrawal // and the account can cross the dormancy threshold while it waits. The savings withdrawal
// below refuses a dormant account anyway, but that failure would surface to the manager as a // below refuses a dormant account anyway, but that failure would surface to the manager as a
// rejected transaction after the approval was recorded; this says why, before anything moves. // rejected transaction after the approval was recorded; this says why, before anything moves.
if (!isGlRequest) {
kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.DEBIT); kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.DEBIT);
}
final BigDecimal amount = new BigDecimal(record.get("amount").toString()); final BigDecimal amount = new BigDecimal(record.get("amount").toString());
final Object ptId = record.get("payment_type_id"); final Object ptId = record.get("payment_type_id");
final Object txnDateObj = record.get("txn_date"); final Object txnDateObj = record.get("txn_date");
@@ -499,11 +469,7 @@ public class ClerkWithdrawalRequestApiResource {
final String description = (String) record.get("description"); final String description = (String) record.get("description");
String txnId = null; String txnId = null;
if (isGlRequest) { if (savingsAccountId != null) {
// Dr the GL account, Cr the branch cash GL -- posted here, at approval, like the savings withdrawal.
validateGlWithdrawalAccounts(toLong(record.get("gl_account_id")), toLong(record.get("contra_gl_id")));
txnId = executeGlWithdrawalJournal(record, amount, ptId, txnDateStr, description);
} else if (savingsAccountId != null) {
final boolean isCurrentAccount = isCurrentAccountById(savingsAccountId, (String) record.get("account_no")); final boolean isCurrentAccount = isCurrentAccountById(savingsAccountId, (String) record.get("account_no"));
txnId = isCurrentAccount txnId = isCurrentAccount
? executeCurrentAccountWithdrawal(savingsAccountId, amount, ptId, txnDateStr, description) ? executeCurrentAccountWithdrawal(savingsAccountId, amount, ptId, txnDateStr, description)
@@ -511,18 +477,14 @@ public class ClerkWithdrawalRequestApiResource {
} }
// Link voucher to the created savings txn and stamp voucher_no on it // Link voucher to the created savings txn and stamp voucher_no on it
if (txnId != null && !isGlRequest) { if (txnId != null) {
voucherService.linkSavingsTransaction( voucherService.linkSavingsTransaction(
VoucherService.REF_CLERK_WITHDRAWAL_REQUEST, id, Long.valueOf(txnId)); VoucherService.REF_CLERK_WITHDRAWAL_REQUEST, id, Long.valueOf(txnId));
} }
// The sides of the slip. A withdrawal is money leaving the member, so their account is // The sides of the slip. A withdrawal is money leaving the member, so their account is
// debited and the till that pays it out is credited. // debited and the till that pays it out is credited.
if (isGlRequest) {
addGlWithdrawalVoucherLines(record, amount);
} else {
addWithdrawalVoucherLines(record, savingsAccountId, amount, txnId); addWithdrawalVoucherLines(record, savingsAccountId, amount, txnId);
}
final String updatedLog = buildActivityLog( final String updatedLog = buildActivityLog(
(String) record.get("activity_log"), "APPROVED", currentUser.getUsername(), null (String) record.get("activity_log"), "APPROVED", currentUser.getUsername(), null
@@ -727,8 +689,8 @@ public class ClerkWithdrawalRequestApiResource {
if (denomMap == null || denomMap.size() == 0) return; if (denomMap == null || denomMap.size() == 0) return;
final Object txnIdObj = record.get("txn_id"); final Object txnIdObj = record.get("txn_id");
final Long txnId = txnIdObj != null ? parseTxnKey(txnIdObj.toString()) : null; final Long txnId = txnIdObj != null ? Long.valueOf(txnIdObj.toString()) : null;
if (txnId == null) return; // no txn linked — cannot record denomination if (txnId == null) return; // no savings txn linked — cannot record denomination
final Object saObj = record.get("savings_account_id"); final Object saObj = record.get("savings_account_id");
final Long savingsAccountId = saObj != null ? Long.valueOf(saObj.toString()) : null; final Long savingsAccountId = saObj != null ? Long.valueOf(saObj.toString()) : null;
@@ -831,117 +793,6 @@ public class ClerkWithdrawalRequestApiResource {
return rows.isEmpty() ? null : rows.get(0); return rows.isEmpty() ? null : rows.get(0);
} }
private static Long toLong(final Object value) {
return value instanceof Number n ? n.longValue() : null;
}
/**
* A savings transaction id is numeric; a GL journal entry's is a hex string. Both are reduced to one
* Long the same way CashierGlDenominationApiResource does, so a GL withdrawal's notes group under
* the id of its own journal entry in the cashier's position and closing report.
*/
private Long parseTxnKey(final String raw) {
if (raw == null || raw.isBlank()) {
return null;
}
try {
return Long.valueOf(raw);
} catch (final NumberFormatException ignored) {
try {
final String hex = raw.length() > 15 ? raw.substring(raw.length() - 15) : raw;
return Long.parseUnsignedLong(hex, 16);
} catch (final NumberFormatException ignored2) {
return null;
}
}
}
/** A GL withdrawal pays cash out of a GL account: the account must not itself be a cash GL, and the
* GL it is paid from must be one of the branch's mapped cash accounts. */
private void validateGlWithdrawalAccounts(final Long glAccountId, final Long contraGlId) {
if (glAccountId == null || contraGlId == null) {
throw apiError(400, "glAccountId and contraGlId are required for a GL withdrawal request");
}
final List<Long> cashGlIds = jdbcTemplate.queryForList(
"SELECT gl_account_id FROM acc_gl_financial_activity_account WHERE financial_activity_type IN (?, ?)",
Long.class, FinancialActivity.CASH_AT_MAINVAULT.getValue(), FinancialActivity.CASH_AT_TELLER.getValue());
if (!cashGlIds.contains(contraGlId)) {
throw apiError(400, "A GL withdrawal must be paid from the branch cash vault GL.");
}
if (cashGlIds.contains(glAccountId)) {
throw apiError(400, "The cash vault itself cannot be the GL account of a withdrawal slip.");
}
}
/** Dr the GL account, Cr the cash GL, through the journal-entry command (not the HTTP API, which is
* switched off for manual postings). Returns the journal entry's transaction id. */
private String executeGlWithdrawalJournal(final Map<?, ?> record, final BigDecimal amount,
final Object paymentTypeId, final String txnDateStr, final String note) {
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
final JsonObject debit = new JsonObject();
debit.addProperty("glAccountId", toLong(record.get("gl_account_id")));
debit.addProperty("amount", amount);
final JsonObject credit = new JsonObject();
credit.addProperty("glAccountId", toLong(record.get("contra_gl_id")));
credit.addProperty("amount", amount);
final JsonArray debits = new JsonArray();
debits.add(debit);
final JsonArray credits = new JsonArray();
credits.add(credit);
final Object office = record.get("office_id");
final Object currency = record.get("currency_code");
final JsonObject payload = new JsonObject();
payload.addProperty("officeId", office instanceof Number n ? n.longValue() : 1L);
payload.addProperty("transactionDate", date);
payload.addProperty("dateFormat", "dd MMMM yyyy");
payload.addProperty("locale", "en");
payload.addProperty("currencyCode", currency != null ? currency.toString() : "INR");
if (note != null && !note.isBlank()) {
payload.addProperty("comments", note);
}
if (paymentTypeId != null) {
payload.addProperty("paymentTypeId", ((Number) paymentTypeId).longValue());
}
payload.add("credits", credits);
payload.add("debits", debits);
final CommandWrapper commandRequest = new CommandWrapperBuilder().createJournalEntry().withJson(payload.toString()).build();
// The request raised its own voucher at submission; the journal command must not raise a second.
final CommandProcessingResult result = voucherService
.withOwnVoucher(() -> commandsSourceWritePlatformService.logCommandSource(commandRequest));
return result.getTransactionId();
}
/** The two sides of an approved GL withdrawal on its voucher: the GL account is debited, the cash GL credited. */
private void addGlWithdrawalVoucherLines(final Map<?, ?> record, final BigDecimal amount) {
final String voucherNo = (String) record.get("voucher_no");
if (voucherNo == null || voucherNo.isBlank()) {
return;
}
try {
final Long glId = toLong(record.get("gl_account_id"));
final Long contraId = toLong(record.get("contra_gl_id"));
final Map<String, Object> gl = glAccountForVoucher(glId);
final Map<String, Object> contra = glAccountForVoucher(contraId);
voucherService.addLines(voucherNo, List.of(
VoucherLine.debitGl(glId, (String) gl.get("name"), (String) gl.get("gl_code"), amount),
VoucherLine.creditGl(contraId, (String) contra.getOrDefault("name", "Cash in Hand"), (String) contra.get("gl_code"), amount)));
} catch (final RuntimeException e) {
LOG.error("Could not record voucher lines for GL withdrawal request voucher {}", voucherNo, e);
}
}
private Map<String, Object> glAccountForVoucher(final Long glAccountId) {
if (glAccountId == null) {
return Map.of();
}
return jdbcTemplate.queryForList("SELECT name, gl_code FROM acc_gl_account WHERE id = ?", glAccountId).stream().findFirst()
.orElse(Map.of());
}
private Map<?, ?> loadRecord(final Long id) { private Map<?, ?> loadRecord(final Long id) {
final List<Map<String, Object>> rows = jdbcTemplate.queryForList(""" final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
SELECT id, account_no, account_type, savings_account_id, client_id, client_name, SELECT id, account_no, account_type, savings_account_id, client_id, client_name,
@@ -949,8 +800,7 @@ public class ClerkWithdrawalRequestApiResource {
txn_date, status, clerk_id, clerk_name, clerk_username, txn_date, status, clerk_id, clerk_name, clerk_username,
manager_id, manager_name, manager_username, rejection_reason, txn_id, manager_id, manager_name, manager_username, rejection_reason, txn_id,
denomination_data, activity_log, submitted_at, actioned_at, denomination_data, activity_log, submitted_at, actioned_at,
cashier_id, disbursed_by, disbursed_at, voucher_no, cashier_id, disbursed_by, disbursed_at, voucher_no
gl_account_id, contra_gl_id, office_id, currency_code
FROM m_clerk_withdrawal_request WHERE id = ? FROM m_clerk_withdrawal_request WHERE id = ?
""", id); """, id);
if (rows.isEmpty()) { if (rows.isEmpty()) {
@@ -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,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 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");
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";
// 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 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,
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,
ACTIVE, LOCALE, DATE_FORMAT);
}
@@ -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,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,186 @@
/**
* 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.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) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
return this.closureService.quote(fdId, type, date(closeDate), receivedAmount, tds, renewalAmount);
}
@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) {
return StringUtils.isBlank(value) ? null : LocalDate.parse(value.trim(), DATE);
}
}
@@ -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,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,74 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.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";
/** 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,184 @@
/**
* 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.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.investment.data.InvestmentInterestScheduleData;
import org.apache.fineract.portfolio.investment.data.InvestmentPurchaseData;
import org.apache.fineract.portfolio.investment.data.InvestmentSaleData;
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;
@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("{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,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,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,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.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;
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()).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,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,68 @@
/**
* 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 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())
.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,72 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.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 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()).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,110 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.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;
@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 existsByScriptId(Long scriptId);
}
@@ -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,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.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;
@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,264 @@
/**
* 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.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.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 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();
}
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);
validateAddress(element, v);
throwIfErrors(errors);
}
// -------------------- 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);
}
}
}

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