Compare commits

..
111 Commits
Author SHA1 Message Date
Anshul Rathore b9aa29fb35 feat: commit latest changes on dev
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 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 Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (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 Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
2026-08-06 10:46:22 +05:30
Anshul Rathore 84f8aa2274 Merge branch 'demo' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend into dev 2026-08-03 12:51:55 +05:30
keshav 7773a9e9c6 feat(aml): add API endpoints and services for manual journal entry referrals
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
2026-08-03 10:49:37 +05:30
keshav c890faee78 form 91/98 report, min max value update in AML rule and whihc rule apply day, week, month and some more updates
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (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-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract 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 Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-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
2026-08-02 11:45:54 +05:30
keshav abc7eb22a2 oss file updated, profitl loss as per the day month and also with date select
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract 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 E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
2026-08-01 11:54:30 +05:30
keshav 8622dcaf90 static update
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
2026-07-31 15:11:23 +05:30
Anshul Rathore 81832b6bb7 feat: add Avro schema definitions for Fineract data entities and update configuration
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - 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 Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - 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-07-31 13:27:40 +05:30
Anshul Rathore 0bd5296519 chore: update JVM memory configuration and add local API integration scripts
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-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 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 - 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-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
2026-07-31 12:41:35 +05:30
Anshul Rathore 17e0dab435 Merge branch 'dev' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend into dev 2026-07-31 12:41:22 +05:30
Anshul Rathore 67810bfaea Add Welcome Kit Dispatch feature and Share Account summary API 2026-07-31 11:51:57 +05:30
keshav 2022dc5241 kyc risk and AML & Compliance Checks automate on client creation
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 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 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 Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
2026-07-31 11:02:31 +05:30
nitesh b1cfa996b4 feat(clients): fromDate/toDate submittedon_date range filter on GET /clients
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (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 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract 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
Backs the Client Approval tab's Today/Yesterday quick filters server-side.
offset/limit/displayName/orderBy/sortOrder/status already existed on this
endpoint; added fromDate/toDate (yyyy-MM-dd, inclusive) alongside them,
filtering on c.submittedon_date, following the same ::date-cast convention
already used by ClientPendingEditReadPlatformServiceImpl for the
equivalent pending-edits filter. Updated the one other caller
(SelfClientsApiResource) to pass null/null.
2026-07-30 10:33:48 +05:30
nitesh 02541eed6c feat(sanctions): expose the loaded list's data-currency date on every screening result
Screening only ever told a caller whether a name matched and when the
check ran — not which dated snapshot of the UN Security Council list it
ran against. Added SanctionsEntryRepository.findListLastLoadedOn()
(MAX(createdDate) over active entries) and threaded it through
SanctionsScreeningResult as listDataAsOf, so the frontend can show "List
Data As Of: <date>" alongside "Screened On".
2026-07-30 10:33:33 +05:30
niteshandClaude Sonnet 5 7c15fbb0c7 fix(documentmanagement): close leaked file handles causing Windows re-upload failures; add entity KYC document CodeValues
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
FileContentStoreService.upload() opened a FileOutputStream (write) and an
InputStream (post-upload MIME check) without closing either. Windows keeps
those handles open for the JVM's lifetime, so the next upsert-style
delete-then-recreate of the same file (e.g. client signature/photo
re-upload) failed with FileSystemException: "being used by another
process". Wrapped both in try-with-resources.

Also adds every distinct non-individual/constitution KYC document name
(HUF, Private/Public Ltd, LLP, Partnership, Trust, Society, Proprietorship,
Association, Govt/PSU, Cooperative) as a KYCIdProofType code value.
resolveDocumentTypeId() on the frontend matches the picked document name
against this list before creating a client_identifier; since these entity
document names had no corresponding code value, every non-individual
client's KYC document upload silently failed to persist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 11:34:18 +05:30
nitesh 6e3f195de0 Merge remote-tracking branch 'origin/uate' into uate
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
2026-07-29 10:31:58 +05:30
niteshandClaude Sonnet 5 a65d801250 feat(clients): maker-checker for client creation and edits (backend)
- Client activation now requires a different user than the one who created the client
  (client.getCreatedBy() vs current user) — no bypass, including CHECKER_SUPER_USER, since a
  bypass would recreate the exact "role bypasses the hold" loophole this closes. Client creation
  itself is unaffected (the wizard needs a real client id immediately for photo/KYC/address/family
  follow-up uploads), only activation is gated.
- New maker-checker queue for Client edits, modelled on this codebase's existing
  m_share_account_pending_edit / chequebook patterns rather than Fineract's native
  command-approval engine (which any ALL_FUNCTIONS role bypasses unconditionally):
  - m_client_pending_edit (migrations 0429-0431): stores the maker's raw edit payload plus a
    point-in-time "before" snapshot (original_json, extracted from the frontend's _originalValues
    key) so the checker's diff stays accurate even after the edit is approved/rejected, when the
    live client no longer reflects the pre-edit state.
  - ClientEditWritePlatformService: submitEditForApproval() stages the edit (refusing a second
    submission while one's already pending, via a nullable-but-unique pending_lock_key);
    approveEdit() replays the payload through the existing updateClient() so all of Client's
    validation/change-detection runs unchanged, just deferred; rejectEdit() discards it. The maker
    can never approve/reject their own submission.
  - ClientEditApiResource: /clients/{id}/edit (submit), /clients/edit/pending (paginated +
    status/clientName/date-range filterable list, mirrors /chequebook/pending-requests),
    /clients/{id}/edit/approve, /clients/{id}/edit/reject. New permissions (migration 0430):
    SUBMITEDIT_CLIENT, APPROVEEDIT_CLIENT, REJECTEDIT_CLIENT, APPROVEEDIT_CLIENT_CHECKER (the
    last one surfaces pending edits in the generic Checker Inbox too).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 10:31:10 +05:30
keshav f45d5890ea CTR/STR report and Risk Category auto mark on bases of person occupation and anual salary enter
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract 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-07-29 10:22:27 +05:30
niteshandClaude Sonnet 5 3c58bb231f fix(clients): pwdCategoryOptions on edit template, minor-age threshold to 18, minor KYC identity-proof bypass
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-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
- ClientData.templateOnTop()/templateWithSavingAccountOptions() never copied pwdCategoryOptions
  from the template — the Disability Type dropdown was always empty on the edit-mode GET
  /clients/{id}?template=true call, even though create-time template() had it.
- Raise the minor-detection threshold in ClientKycApiResource from age <10 to <18 (legal
  definition), mirroring the frontend's own rule so the required-documents list served here
  stays consistent.
- ClientKycCommand.validateForInitiate() unconditionally required at least one of
  PAN/Aadhaar/GSTIN, with no awareness of the Birth Certificate/School ID document-type picker
  minors actually use — blocking "Save & Next" on the KYC tab for every minor client. Added a
  skipIdentityDocumentCheck flag, set from client.getIsMinor() at both call sites
  (initiateKyc/initiateKycInline).
- Added Birth Certificate and School ID Card as KYCIdProofType code values — they were already
  offered as document-type options for minors but never existed in the code-value list that
  saveKycDocProof() resolves against, so selecting either silently failed to save any
  identifier/document.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 15:51:08 +05:30
niteshandClaude Sonnet 5 aabda18b69 feat(clients): enforce Mother + Father/Caretaker family members on client activation
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
- Add CARETAKER as a new RELATIONSHIP code value (previously missing — a client with no
  father had no valid way to satisfy the rule below).
- Add ClientFamilyMembersWritePlatformService.validateFamilyRequirementsForActivation(),
  called from activateClient(): requires an active "Mother" family member, plus either
  "Father" or "Caretaker", for individual clients (non-individual entities are exempt).
- Two distinct exceptions (ClientFamilyMemberMotherRequiredException /
  ClientFamilyMemberFatherOrCaretakerRequiredException) so the two failure reasons surface
  as separate errors instead of one merged message, mirroring the frontend's separate banners.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 12:37:24 +05:30
nitesh 0e5671b706 Merge remote-tracking branch 'origin/uate' into uate
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
2026-07-28 10:55:05 +05:30
niteshandClaude Sonnet 5 67214ec0f8 fix(clients): stale signature bug, CKYC fixes, monthly turnover/annual income, senior citizen/PWD classification
- Add m_client.signature_id (single-slot pointer, mirrors image_id) plus
  ClientSignatureIdAdapter/EntitySignatureIdAdapter so re-uploading a signature updates the
  existing m_document row in place instead of leaving duplicate rows behind that readers picked
  inconsistently.
- Fix CkycBatchBuilderServiceImpl address-proof classification to read the client identifier's
  actual m_code (KYCAddressProofType vs KYCIdProofType) instead of a hardcoded string set that
  disagreed with the CKYC tab and silently dropped Aadhaar-as-address-proof identifiers.
- Organize the CKYC ZIP into per-artefact-type folders (PHOTO/SIGNATURE/ID_PROOF/ADDRESS_PROOF).
- Fix CkycBatchBuilderServiceImpl.createBatch() using the daily batch sequence number as the ZIP
  document's parent_entity_id instead of the real m_ckyc_batch.id, which 404'd on download
  whenever the two diverged (i.e. almost always).
- Add monthlyTurnover and annualIncome fields alongside the existing annualTurnover/monthlyIncome.
- Add isSeniorCitizen, isPwd, and pwdCategory (proper m_code_value-backed PwdCategory code, not a
  free-text column) to m_client, following the same pattern as occupation/religion/marital status.
- Add Diploma as its own Education Level (with diploma-specific DegreeQualification values) and
  Graduate as a level distinct from Undergraduate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 10:50:16 +05:30
keshav 026d902199 minor change on client
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
2026-07-28 10:43:06 +05:30
nitesh bc30241950 Merge remote-tracking branch 'origin/uate' into dev
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 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
2026-07-27 14:24:16 +05:30
keshav f18b5d97d3 share certificate and share product maker checker
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 Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract 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 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
2026-07-27 14:14:05 +05:30
niteshandClaude Sonnet 5 14df914b9a feat(clients): dual-OTP verified contact changes with customer notification
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Mobile number and email address changes now require OTP verification on
both the old and new destination (staff-entered) before the update is
accepted, at both client creation and edit time — closing a gap where an
unverified wrong number/email could never be corrected later. Every
mobile/email/address change also emails the customer at their pre-change
address so an unauthorized change is never silent.

- New ClientContactOtpService (+Impl) mirrors the existing login OTP
  services, reusing OtpLog with a CLIENT_CONTACT_CHANGE purpose.
- New sibling API resources for edit (/clients/{id}/contact-verification/otp)
  and create (/clients/contact-verification/otp, no clientId yet).
- ClientWritePlatformServiceJpaRepositoryImpl / AddressWritePlatformServiceImpl
  gate the actual field change on verified OTPs and fire
  ClientChangeNotificationService afterwards; the notification recipient is
  always snapshotted before any mutation so a same-request mobile+email
  change can't accidentally alert the new (unverified-by-its-real-owner)
  address instead of the old one.
- ClientDataValidator validates OTP id format only (ignoreIfNull) —
  enforcement lives solely in the write-service's diff-gated calls, since
  edit-client always PUTs the full client object regardless of what changed.
- 0420 migration seeds 8 CLIENTKYC action permissions
  (GenerateAadhaarOtp/VerifyAadhaar/VerifyPan/VerifyCin/VerifyDin/
  VerifyGstin/UploadCkyc/CompleteRekyc) that were referenced by existing
  command handlers but never seeded, causing a 404 for every user including
  Super User.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 14:36:48 +05:30
niteshandClaude Sonnet 5 5dc175c5e1 feat(users): auto-enroll admin-chosen OTP channel as MFA factor at creation
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 Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 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
Admin now picks a mandatory OTP delivery method (Email/Mobile) when creating a
user. The channel is enrolled immediately as a verified m_user_mfa_factor row,
so the existing 2FA system (MfaFactorBackedTwoFactorServiceImpl + /twofactor)
challenges the user for OTP on their very first login - reusing the
already-built login-OTP infrastructure instead of a parallel flow.

Also adds a versioned, admin-manageable user-agreement (disclaimer) API
(/v1/useragreements) with per-user acceptance tracking, checked right after
every login, plus a Liquibase cleanup migration and a shared OTP-destination
masking utility extracted from the two existing login-OTP services.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 16:16:40 +05:30
nitesh cf60515bf6 Merge remote-tracking branch 'nexora/uate' into merge-uate-into-dev
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (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 - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (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 E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-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
# Conflicts:
#	.gitignore
#	fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
#	fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/FacilityChargeAutoApplyService.java
2026-07-23 17:36:10 +05:30
niteshandClaude Sonnet 5 a098f8ba75 fix(chequebook): prevent cheque-leaf serial collisions across series at issuance time
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract 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
The real root cause behind the leaf-serial collision bug: stock-lot
overlap checking was scoped to "within the same series only"
(ChequeStockLotRepository.countOverlappingInSeries), on the incorrect
assumption that two different cheque designs never share a numeric
range. A real bank allocates leaf serials from one bank-wide numbering
authority, so this is exactly what let two series end up with
overlapping ranges that, once both were issued to the same account,
produced an unresolvable serial collision (NpciInwardPushService's
drawer-account-number disambiguation can't help when every colliding
leaf belongs to the same account).

Widened the check to countOverlappingAnySeries — both the maker-submit
and checker-approval paths in ChequeBookWritePlatformServiceImpl now
reject a stock receipt whose range overlaps ANY existing lot, regardless
of series.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 17:24:17 +05:30
Jaanu423andClaude Sonnet 5 ab5666c0f4 feat(ctsclearing): signature-verification gate and full NPCI field capture for inward cheques
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Adds a manual signature-verification step to the inward CTS decision flow:
a new VERIFY_SIGNATURE command records the clerk's confirmation, and
InwardClearingItem.pass() now refuses to pass a cheque until signatureOk
is true — enforced server-side, not just a disabled UI button.

Fixes a real bug where ChequeLeaf.leafSerialNo collides across different
cheque books/series (by design — serials are only unique per stock lot),
which could resolve an inward item to the wrong account. NpciInwardPushService
now disambiguates using the drawer account number NPCI already sends
alongside the cheque number.

Also closes a completeness gap found by auditing the outward-CBS -> NPCI ->
inward-CBS pipeline: five fields NPCI sends on every instrument (drawer
account number, deposit account number, instrument type/description, drawee
bank name, per-check IQA breakdown) were read and discarded instead of
persisted — now saved via new columns and exposed on the read API.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 12:30:07 +05:30
keshav 84b1685873 Add customer search and sponsor DD reconciliation modules; stop tracking logs
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 Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-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 Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Adds customersearch and sponsordd portfolio modules with associated
changelog parts, updates cashier deposit request handling, and stops
tracking fineract-provider/logs/.
2026-07-22 11:53:32 +05:30
Jaanu423andClaude Sonnet 5 b433edfdfe feat(clearing): add CTS/NACH clearing modules with real NPCI integration
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
- Inward/outward CTS clearing (capture, validate, settle, dishonour), positive pay,
  settlement/reconciliation, and NACH clearing modules
- Real, separate NPCI clearing-house integration: hybrid RSA+AES envelope crypto,
  bank self-registration, outward batch submission, and an inward push webhook
- Outward clearing is now session-batched: a 2-hourly scheduled job (plus the
  existing manual "Send Pending Batch" fallback) bundles CAPTURED cheques instead
  of sending them one at a time
- Batch-summary read APIs and date/text filters across inward batches, outward
  presentments, and outward batches

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 15:46:52 +05:30
Jaanu423andClaude Sonnet 5 383dfdc6e9 feat(chequebook): silent balance hold instead of activation-time charge
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 - 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 Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - 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
- Removed the "CHEQUE_BOOK" facility charge from account-activation
  auto-apply (both savings and current) — the per-leaf issuance charge is
  now the only cheque-book charge, applied at request/approval time only.
- Cheque book request now reserves the charge amount via a direct
  on-hold-funds field update instead of Fineract's native hold-amount
  command, so no "Amount on hold"/"Release Amount" transaction row is ever
  created — nothing shows in the passbook/transactions list, and the
  reservation is released the same way (silently) on approval (then
  charged) or rejection (returned), for both savings and current accounts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 15:03:30 +05:30
Jaanu423andClaude Sonnet 5 5ed95c27e8 fix(chequebook,current-account): current-account transaction fixes for cheque-book charge
- CurrentAccountTransactionsApiResource.retrieveOne() was calling the savings
  read-service to fetch a single transaction, which always queries
  m_savings_account_transaction and can never find a current-account row —
  crashed with EmptyResultDataAccessException. Added a dedicated
  CurrentAccountReadPlatformService.retrieveOneTransaction() backed by
  m_current_account_transaction instead.
- CurrentAccountReadPlatformServiceImpl.buildTransactionType() (and the
  search-transactions endpoint) had no cases for hold (8) / release (9)
  transaction types, so they displayed as "Deposit"/"Other" instead of
  "Amount on hold"/"Release Amount".
- ChequeBookIssuanceChargeService.deductFromCurrent() never set
  runningBalanceDerived on the charge-deduction transaction, leaving the
  Balance column blank for that row in the transactions list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 15:06:35 +05:30
Jaanu423andClaude Sonnet 5 fde670e644 feat(chequebook): per-leaf issuance charge with balance validation and fund hold
Requesting a cheque book now computes a per-leaf charge (reusing the existing
"Cheque Book Charge" facility mapping) and blocks the request at submit time if
the account can't absorb it without breaching its minimum required balance.

The charge amount is reserved via Fineract's native hold-amount mechanism when
the request is submitted, released and actually deducted (with GL posting) on
checker approval, or released back to the customer on rejection — so funds are
never left dangling on hold regardless of outcome.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 11:19:11 +05:30
Jaanu423andClaude Sonnet 5 3bb7fc8050 feat(chequebook): add RETURNED leaf status for dishonoured/bounced cheques
Adds a maker-checker-gated "return" command on cheque books that flags
presented-and-dishonoured leaves as terminal RETURNED (distinct from
STOP_PAYMENT/drawer-initiated and LOST/custody-failure), following the
same reason/actor/timestamp pattern already used for lost/cancelled/used
leaves on m_cheque_leaf.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 16:07:04 +05:30
Jaanu423 04d65d857f feat(chequebook): add cheque book module with Maker-Checker workflow
Adds cheque series, stock lot/transfer, cheque book issuance,
stop-payment/lost/cancel-leaf actions, and pending-request approval
APIs with Maker-Checker command handlers and liquibase changelogs.
2026-07-15 11:13:06 +05:30
keshav898966 b064ee6307 existing account saving and current add or remove nominee and digital & account features and also FE & RD nominee add or remove 2026-07-10 18:40:16 +05:30
Jaanu423andClaude Sonnet 4.6 9d09d9ebd4 fix(db): add missing m_saving_product_temp table causing 500 on product list APIs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 15:58:17 +05:30
Jaanu423andClaude Sonnet 4.6 eb88fb0813 fix(db): add missing m_savings_account columns causing 500 on savings list
introducer_required, share_linking_enabled, terminate_true were missing
from m_savings_account in fineract_saga. Applied directly to DB and
added 0368 catch-up migration with preConditions for idempotency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 15:47:14 +05:30
Jaanu423andClaude Sonnet 4.6 f13a9c6bba merge(release): merge feature/minor-update into release/share-account-module
Combines election module + UAT catch-up migrations (0338-0367) with
minor-update changes: soft delete for tellers (0338), share linking
for current account (0339), terminate flag for savings/current (0340).
Resolved changelog conflict by appending minor-update entries after 0367.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 13:43:03 +05:30
Jaanu423andClaude Sonnet 4.6 0e076889e9 feat(db): idempotent catch-up migrations for UAT schema gaps (0358-0367)
Add 10 Liquibase catch-up changeSets covering m_client, m_client_kyc,
m_family_members, m_client_nominee, m_client_msme, m_rbi_feed_item,
m_savings_product, m_current_product, m_clerk_withdrawal_request,
m_daily_token_counter, and misc column additions (0207-0348 range).
All changeSets use preConditions (tableExists/columnExists) so they
MARK_RAN if already applied. Fixes PostgreSQL startup crash in 0360
by replacing MySQL-specific INFORMATION_SCHEMA sqlCheck with Liquibase-
native foreignKeyConstraintExists.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 13:35:41 +05:30
Jaanu423andClaude Sonnet 4.6 87b308b527 feat(election): Chairman/VC auto-resolution, member vote list on voting page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 10:59:00 +05:30
Jaanu423andClaude Sonnet 4.6 934a51c9ee feat(election): dynamic designations from m_code, email-based voting, vote remark field
- DesignationService reads designation limits and mandatory flags from DirectorDesignation m_code_value
- DirectorAppointmentServiceImpl: replaced hardcoded DESIGNATION_LIMITS map with dynamic DB fetch
- CommitteeServiceImpl: replaced hardcoded Chairman/Vice Chairman check with dynamic mandatory designation query
- New GET /v1/election/directors/designations endpoint returns all active designations
- Email-based voting module: MeetingPublicVoteApiResource, MeetingVoteAdminApiResource, MeetingVoteService
- MeetingMemberVote: added remark (TEXT) column for member vote remarks
- AgendaVoteRequest: added remark field to carry voice/typed remarks per agenda item
- Liquibase migrations 0354 (vote/resolution tables), 0355 (DirectorDesignation code), 0356 (vote remark column)
- SecurityConfig: permit public voting endpoints without authentication

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 18:26:14 +05:30
keshav898966 9980310c9e table changes on Maker-Checker workflow for Products, with approval-based updates and product locking to prevent concurrent edits 2026-07-06 17:49:39 +05:30
Jaanu423andClaude Sonnet 4.6 cfcd24efad refactor(client): reformat ClientMapper.java
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-06 16:58:34 +05:30
Jaanu423andClaude Sonnet 4.6 73db4ee114 feat(election): relational agenda items, email redesign, and stale-cache fix
- Add CommitteeMeetingAgenda entity + repository for relational agenda storage
- Liquibase migration 0353: create m_election_committee_meeting_agenda table,
  drop old agenda TEXT column from m_election_committee_meeting
- Update CommitteeMeeting: replace String agenda with List<CommitteeMeetingAgenda>
  (EAGER, no cascade — writes handled explicitly via repository)
- Update CommitteeMeetingRepository: explicit JPQL with SELECT DISTINCT + LEFT JOIN
  FETCH to load agendaItems and prevent NonUniqueResultException on Optional returns
- Fix email sending stale agenda: inject EntityManager, call refresh() after
  agendaRepository.flush() so EclipseLink reloads the collection from DB
- Redesign buildMeetingInviteHtml: table-based email layout, icon+label+value rows,
  amber agenda section with numbered badges, compact two-column footer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-06 16:53:32 +05:30
Jaanu423andClaude Sonnet 4.6 e72b9359c1 feat(election): committee meeting module — schedule, online/offline mode, soft delete, audit logging
- CommitteeMeeting entity, enums (ONLINE/OFFLINE mode, SCHEDULED/COMPLETED/CANCELLED status), repository with soft delete filter
- CommitteeMeetingService + Impl: schedule, update, patchStatus, soft delete, retrieveByCommittee
- CommitteeMeetingApiResource: REST endpoints at /v1/election/committee-meetings with full audit logging
- ElectionAuditAction: add SCHEDULE_MEETING, UPDATE_MEETING, CANCEL_MEETING, COMPLETE_MEETING, DELETE_MEETING + FAILED variants
- CommitteeService: expose getElectionIdForCommittee() for audit context
- Liquibase 0351: create m_election_committee_meeting table with is_deleted soft delete column
- Liquibase 0352: rename created_date/last_modified_date → created_on_utc/last_modified_on_utc to match EclipseLink base class mapping

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-06 10:16:20 +05:30
Jaanu423andClaude Sonnet 4.6 a1a31aff25 feat(election): comprehensive audit log fixes — committee logging, deduplicate service logs, fix director entity type
- Add audit logging for all 5 committee operations (create, update, dissolve, add/remove member)
- Add CREATE_COMMITTEE, UPDATE_COMMITTEE, DISSOLVE_COMMITTEE, ADD_COMMITTEE_MEMBER, REMOVE_COMMITTEE_MEMBER to ElectionAuditAction enum
- Fix double logging: remove all duplicate audit calls from ElectionWritePlatformServiceImpl (API resource logs are authoritative)
- Fix double logging in DirectorAppointmentServiceImpl (keep only autoExpireByTenure scheduler log)
- Fix entity type bug in DirectorAppointmentApiResource: "DIRECTOR" -> "DIRECTOR_APPOINTMENT" to match frontend filter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 17:24:03 +05:30
Jaanu423andClaude Sonnet 4.6 2606427203 feat(election): committee management — CRUD, members, image, and director fix
- Add Committee entity, CommitteeMember, repositories, service, and REST API
- Liquibase migrations: 0349 committee tables, 0350 committee image column
- Committee supports profile image (base64 TEXT), name, description, status
- Current directors: fix EclipseLink native query (?1 positional param) and add
  fallback to most recent RESULT_DECLARED election when no active duration found
- DirectorAppointmentRepository: add findByElectionIdAndStatus (non-pageable)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 16:39:42 +05:30
keshav898966 b0bfa386ce implemented a Maker-Checker workflow for Products, with approval-based updates and product locking to prevent concurrent edits 2026-07-03 15:15:09 +05:30
Jaanu423andClaude Sonnet 4.6 00d3a2e333 feat(election): candidate modification lock, director member photo, and comprehensive audit logging
- ElectionConfig: add isCandidateModificationLocked() — blocks remove/update/symbol after voting starts
- ElectionWritePlatformServiceImpl: full audit logging on all 16 operations; failures logged in new transaction (REQUIRES_NEW) so they survive main tx rollback
- DirectorAppointmentData: add memberId field for client photo lookup
- DirectorAppointmentServiceImpl: batch-fetch candidate memberId via ElectionCandidateRepository; pass to response

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 13:44:49 +05:30
Jaanu423andClaude Sonnet 4.6 048a4ecfde feat(election): election duration blocking, director tenure auto-fill, audit logging, and scheduler
- Add durationYears to ElectionConfig with Liquibase migration (0348)
- Block new election creation when previous election duration is still active (PostgreSQL-compatible native query)
- Auto-override director tenureEndDate from election durationEndDate on appoint and update
- Add ElectionSchedulerConfiguration to enable @Scheduled support
- Fix JPQL EclipseLink enum compatibility in expireByTenureDate query
- Add full audit logging to DirectorAppointmentServiceImpl (appoint, update, remove, auto-expire)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 12:25:43 +05:30
Jaanu423andClaude Sonnet 4.6 7ce7353e7a feat(election): Board of Directors — director appointment management
- DirectorAppointment entity + Liquibase migration (0347)
- CRUD API: POST/GET/PUT/DELETE /v1/election/directors with @Valid Bean Validation
- GET /v1/election/directors/pending — WON candidates not yet appointed
- Duplicate prevention: existsByElectionIdAndCandidateId check on appoint
- Business validation: tenure end must be after start; tenure start cannot be before appointment date
- @Scheduled daily midnight job to auto-expire tenure-ended directors via bulk JPQL UPDATE
- Full audit logging for all director actions (success + failure) via ElectionAuditService
- PendingDirectorData DTO; DirectorAppointmentStatus enum (ACTIVE/RESIGNED/REMOVED/TENURE_ENDED)
- ElectionAuditAction: added APPOINT_DIRECTOR, UPDATE_DIRECTOR_APPOINTMENT, REMOVE_DIRECTOR_APPOINTMENT + FAILED variants
- ElectionResultRepository: findByResult(String) for pending director lookup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 12:50:04 +05:30
keshav898966 99c153830c pending account terminate feature develop 2026-07-01 17:25:04 +05:30
Jaanu423andClaude Sonnet 4.6 ba199d78e7 feat(election): date validations, overdue-only NDC/loan checks, audit log gaps fixed
- Added ElectionDateViolationException for proper 4xx on date violations
- createElection: validates date order (nominationStart <= nominationEnd <= electionDate <= resultDate)
- withdrawNomination/resubmitNomination: blocked after nominationEndDate
- closeNominations: requires today >= nominationEndDate
- openVoting/closeVoting: requires today >= electionDate
- declareResults: requires today >= resultDate (or electionDate if no resultDate)
- NDC hasNoDues: checks overdue EMIs only (total_overdue_derived), not outstanding balance
- Director/guarantor/family loan checks: overdue-based only — pass if no missed EMIs
- MemberHasOutstandingDuesException: proper AbstractPlatformDomainRuleException (was IllegalStateException → 500)
- Audit gaps fixed: RESUBMIT_NOMINATION_FAILED added to enum, try/catch added for resubmit,
  createElection, updateElection; saveManualVoteCounts catches all RuntimeException

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 16:50:08 +05:30
Jaanu423andClaude Sonnet 4.6 99cc972804 feat(election): comprehensive audit log — rich details for all actions
- Fixed VARCHAR(500) overflow: description column changed to TEXT (Liquibase 0346)
- Fixed CEO pre-check 500 error: fm.firstname/lastname column name fix
- Fixed audit log showing wrong PASS/FAIL for CEO pre-check
- Fixed approval action not showing step name/number (read PENDING step before action)
- submitNomination: details JSON with applicantName, memberId, postId, gender, caste
- runEligibilityCheck: details JSON with all 5 check results + remarks
- runCeoPreCheck: details JSON with all 7 individual check results + remarks
- processApprovalStep: details JSON with action, stepName, stepNumber, remarks, rejectionReason
- activateElection/closeNominations/openVoting/closeVoting/cancelElection: details with electionTitle + newStatus
- updateElection: full request JSON via ObjectMapper
- approveCandidate: details with nominationId + applicantName
- updateCandidate: details with symbolUpdated, profileUpdated, briefProfile
- uploadCandidateSymbol: details with filename + mimeType
- declareResults: winner summary JSON array [{post, winner, votes}]
- saveManualVoteCounts: return type changed void→String, details JSON array with candidate vote data
- Added Liquibase migrations: 0344, 0345 for manual vote count, 0346 for TEXT column

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 14:59:23 +05:30
Jaanu423andClaude Sonnet 4.6 99a89d5d81 feat(election): overall shareholder analysis API + analytics enhancements
- ElectionAnalyticsServiceImpl: fix SQL_FAMILY to count candidate's own family members who are active shareholders (candidate-specific, not system-wide)
- ElectionAnalyticsServiceImpl: SQL_SUB_CASTE now groups by m_client.lastname instead of sub_caste_cv_id
- ElectionAnalyticsServiceImpl: SQL_CANDIDATE_INFO fetches candidate client_id, lastname, religion for match highlighting
- ElectionAnalyticsServiceImpl: add getOverallShareholderAnalysis() — returns all demographic breakdowns with no candidate matching
- ElectionAnalyticsService: add getOverallShareholderAnalysis() interface method
- ElectionCandidateApiResource: add GET /{electionId}/shareholders/analysis endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 18:36:02 +05:30
Jaanu423andClaude Sonnet 4.6 388af0a6a3 feat(election): candidate voter analysis API — multi-dimension breakdown + sub-caste migration
- New ElectionAnalyticsServiceImpl with 14 SQL breakdown queries:
  gender, age, area, social category (caste), sub-caste, religion,
  occupation, share-holding, tenure, loan, education, marital status,
  monthly income, savings account depth
- Cross-dimension match: caste + area simultaneously (core voter base)
- Zero-count row injection for caste/gender/sub-caste when candidate's
  category has no shareholder matches in GROUP BY result
- Liquibase 0343: sub_caste_cv_id column on m_client with 33 pre-seeded
  sub-caste values across General / OBC / SC / ST categories
- Liquibase 0342: election post eligibility columns
- CandidateVoterAnalysisData DTO with education, marital, income,
  savings, activeSavingsHolders, crossMatchCount fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 11:54:01 +05:30
keshav898966 1bd41cff43 saving, currun, fd and rd account working and tested 2026-06-30 10:06:59 +05:30
Jaanu423andClaude Sonnet 4.6 34f84e1fc6 chore: remove tracked log files and ignore logs directory permanently
- Delete all fineract-provider/logs/*.log from version control
- Add fineract-provider/logs/ to .gitignore so runtime logs are
  never tracked again

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 13:06:54 +05:30
Jaanu423andClaude Sonnet 4.6 69513f25f3 feat(election): production-ready audit log with server-side filter + pagination
- Add m_election_audit_log table via Liquibase (0341)
- ElectionAuditLog entity, ElectionAuditAction enum (21 actions)
- ElectionAuditService: log() with double try-catch, getAuditLogPaged()
  with entityType/action/full-text filters, size cap at 100, page safety
- ElectionAuditLogRepository: findWithFilters() JPQL with optional params
- All 4 election API resources wired with audit calls (create/update/
  activate/nominations/voting/candidates/votes/results)
- GET /{electionId}/audit-log accepts ?entityType=&action=&search=&page=&size=

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 12:57:21 +05:30
Jaanu423andClaude Sonnet 4.6 3d30bdb788 feat(election): CBS ContentStore symbol upload, hasSymbol DTO, vote summary API
- ElectionCandidateData: replace String symbol with Boolean hasSymbol + add memberId
- ElectionCandidateApiResource: symbol upload/download via ContentStoreService
  (mimeType|storagePath format, backward-compatible with legacy base64 records)
- ElectionVotingApiResource: add GET /vote-summary endpoint (totalVotes, totalVoters,
  per-candidate breakdown) using ElectionVoteRepository queries
- ElectionVoteRepository: add countByElectionConfigId and voteSummaryByCandidate queries
- ElectionReadPlatformServiceImpl: map hasSymbol and memberId in candidate list response
- ElectionWritePlatformServiceImpl: set photoUrl from memberId on approveCandidate,
  delete old symbol file from ContentStore on removeCandidate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 18:39:28 +05:30
Jaanu423andClaude Sonnet 4.6 ed139d6e68 feat(election): server-side pagination, family loan CEO pre-check, extended checks
- PagedResponse<T>: generic paginated wrapper with statusCounts and summary fields
- ElectionConfigRepository: pageable query + countGroupByStatus
- ElectionNominationRepository: pageable queries + count-by-status helpers
- ElectionCandidateRepository: pageable queries by election and post
- ElectionReadPlatformService: all list methods return PagedResponse with page/size params
- ElectionReadPlatformServiceImpl: add missing Pageable import; paginated impls with summary data
- API resources: @QueryParam page/size with @DefaultValue on all 3 list endpoints
- ElectionCeoPrecheck: add familyLoanCheckResult + familyLoanCheckRemarks (7th auto-check)
- ElectionCeoPreCheckServiceImpl: fix directorLoan check (overdue only); add family loan check covering manual members (unverifiable→FAIL) and linked CBS clients (overdue→FAIL)
- Liquibase 0338/0339: extended CEO pre-check columns + family loan check columns
- changelog-tenant.xml: include both new changesets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 12:08:29 +05:30
keshav898966 db41c2b606 feat: add share linking enabled to current and savings accounts 2026-06-26 11:12:00 +05:30
Jaanu423andClaude Sonnet 4.6 fc80869d4b feat(guarantor): enrich guarantor accounts API with loan health and borrower details
- Add borrower name, savings account, guaranteed/released/remaining amounts, loan timeline, and overdue info to GuarantorAccountSummaryData
- Fix duplicate rows in GuarantorLoanAccountSummaryDataMapper by joining savings account via guarantor funding details instead of client ID
- Fix NPE in GuarantorDomainServiceImpl when loan product guarantee details are null

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 13:15:25 +05:30
keshav898966 8e55b6747c Merge branch 'feature/minor-update' after resolving conflict in logs 2026-06-24 12:50:35 +05:30
keshav898966 21878939b5 Merge branch 'feature/minor-update' of https://git.zeptic.in/CBSNexora/JAVA-Backend-CBS-MAIN into feature/minor-update 2026-06-24 12:47:53 +05:30
keshav898966 b5c9101fa4 feat: implement teller creation limit and soft delete with cashier check bypass 2026-06-24 12:47:03 +05:30
keshav898966 1ff3f9c0c9 shares account link to new saving account 2026-06-23 15:23:30 +05:30
Jaanu423andClaude Sonnet 4.6 1eba2c50ed fix(election): move additional permissions to new changeset to avoid Liquibase checksum error
The 5 missing permissions were added to the already-executed changeset
causing checksum mismatch on restart. Move them to a new changeset
0337_seed_election_permissions_v2 with a preCondition guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 11:13:39 +05:30
Jaanu423andClaude Sonnet 4.6 3367b7d935 fix(election): seed missing permissions in Liquibase migration 0337
Add 5 permissions that the write service enforces but were absent from
the DB seed: UPDATE_ELECTION, ELIGIBILITY_CHECK_ELECTION,
CEO_PRECHECK_ELECTION, APPROVE_ELECTION_NOMINATION,
APPROVE_ELECTION_CANDIDATE — without these, role assignments for those
operations would fail at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 10:48:46 +05:30
Jaanu423andClaude Sonnet 4.6 5899d2ca07 feat(election): add election module — config, nominations, candidates, voting, approval flow
- REST API resources: ElectionConfig, ElectionNomination, ElectionCandidate, ElectionVoting
- Domain entities, repositories, services (read + write) with maker-checker approval steps
- Eligibility checks, no-due-certificate, CEO pre-check services
- Liquibase migration 0337 creates all election module tables

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 17:39:43 +05:30
Jaanu423andClaude Sonnet 4.6 c1e23080fc fix(share-account+clerk): remove cashier collection validation, add NPE guards
ShareAccountWritePlatformServiceJpaRepositoryImpl:
- Remove m_share_cashier_collection validation from activateShareAccount() —
  cashier now enters GL directly via transaction entry workstation
- Mark all share account charges as paid on activation instead of posting
  duplicate GL (cashier already posted DR Cash -> CR Charges GL directly)
- Remove unused imports: java.sql.Date, EmptyResultDataAccessException, Collectors
- Update createShareAccount() comment to reflect new cashier GL flow

ClerkDisbursementRequestApiResource:
- Add null guard for clerk_id before cast in approve() — returns 500 with
  clear message if DB record is missing the field
- Add null/blank guard for credit_entries in approve() — returns 400 with
  clear message instead of NPE on gson.fromJson

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 18:46:17 +05:30
Jaanu423andClaude Sonnet 4.6 7354da8c1e feat(bank-transfer): add maker-checker support for bank transfers
- Added request_type column to m_clerk_disbursement_request via migration 0336
- submit() extracts requestType from body, stores in DB (defaults to DISBURSEMENT)
- approve() detects BANK_TRANSFER mode via request_type field:
    - DR entries: directDebitSavingsAccount / directDebitCurrentAccount
    - CR entries: directCreditSavingsAccount / directCreditCurrentAccount
    - GL: DR SavingsControl(per DR) / CR SavingsControl(per CR)
    - Audit INSERT into m_bank_transfer_log with unique BT voucher number
    - Existing disbursement/collection logic unchanged in else branch
- list(), myRequests(), loadRecord() all return request_type
- Migration 0335 (m_bank_transfer_log) also included

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:47:21 +05:30
Jaanu423andClaude Sonnet 4.6 bf5ce421df feat(clerk+gl): implement real CBS book transfer for savings and current account disbursements
- Replace Fineract cash deposit API with direct INSERT for both savings and current accounts
  → Zero CashVault entries created anywhere in DB for disbursements
- Savings: directCreditSavingsAccount now sets running_balance_derived and updates
  total_deposits_derived alongside account_balance_derived (parity with current account)
- Current: directCreditCurrentAccount — direct INSERT into m_current_account_transaction
  with running_balance_derived, total_deposits_derived update, and remarks audit field
- getCurrentControlGlId: looks up SAVINGS_CONTROL GL for current account products
  via acc_product_mapping (product_type=2, financial_account_type=2)
- directReverseCurrentCredit: compensation restores both account_balance_derived
  and total_deposits_derived on rollback
- GL: combined DR sourceGL / CR SavingsControl entry posted for both savings and
  current account credits; linkGlEntriesToSavingsAccounts sets entity_type_enum=2
  so ACCOUNT NO column shows in journal entries view
- Hide reversed=true GL entries from all GL views in JournalEntryReadPlatformServiceImpl

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 11:04:17 +05:30
Jaanu423andClaude Sonnet 4.6 d002fc0c9b feat(clerk): add collection flow backend — savings withdrawal + Cash@Vault GL offset
- ClerkDisbursementRequestApiResource: support collection mode (dbCr='Dr' entries); submit() separates DR/CR totals and validates balance; approve() detects mode, calls executeSavingsWithdrawal/executeCurrentAccountWithdrawal for DR entries, then posts GL offset via findCashVaultGlId(txnRefs, type_enum=1) → target GL
- ClerkWithdrawalRequestApiResource: minor updates
- DB migration: changelog-tenant.xml + 0333_rename_disbursed_to_withdrawn.xml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 15:27:32 +05:30
Jaanu423andClaude Sonnet 4.6 a6eae3d6e8 feat(clerk): add GL disbursement maker-checker backend
- Add ClerkDisbursementRequestApiResource with submit/list/my/approve/reject endpoints
- Approve posts savings/current deposits then offsets DR source GL / CR Cash at Vault
- Add Liquibase migration for m_clerk_disbursement_request table (IF NOT EXISTS indexes)
- Add VoucherService constants TYPE_DISBURSEMENT and REF_CLERK_DISBURSEMENT_REQUEST

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 10:44:04 +05:30
Jaanu423andClaude Sonnet 4.6 3e1abaef03 fix(cashier): fix NumberFormatException on GL deposit approval and add GL maker-checker flow
- Use parseTxnIdToLong() for voucherService.linkSavingsTransaction() to
  handle hex GL txnId (e.g. "a2070e542bab") without NumberFormatException
- Add GL deposit maker-checker: submit stores gl_account_id, contra_gl_id,
  office_id, currency_code; approve routes to executeGlJournalEntry()
- Save denomination at submit time (cash physically received); update
  txn_id to real Fineract ID on approve, delete on reject
- Add Liquibase migration 0331 to add GL columns to m_cashier_deposit_request

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 14:59:28 +05:30
Jaanu423andClaude Sonnet 4.6 40449ef0f7 fix(cashier+clerk): route CA deposits and withdrawals to current account commands
CashierDepositRequestApiResource:
- approve(): use isCurrentAccountById() to detect current accounts, route to
  executeCurrentAccountDeposit() instead of executeSavingsDeposit()
- Add executeCurrentAccountDeposit() using CommandWrapperBuilder.currentAccountDeposit()
- Add isCurrentAccountById() helper querying m_current_account table
- Add POST /{id}/reprocess endpoint to fix already-approved CA records that
  were wrongly posted to savings account

ClerkWithdrawalRequestApiResource:
- approve(): use isCurrentAccountById() to detect current accounts, route to
  executeCurrentAccountWithdrawal() instead of executeSavingsWithdrawal()
- Add executeCurrentAccountWithdrawal() using CommandWrapperBuilder.currentAccountWithdrawal()
- Add isCurrentAccountById() helper querying m_current_account table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 12:52:09 +05:30
Jaanu423andClaude Sonnet 4.6 33ea10689f feat(cashier+voucher): add cashier deposit maker-checker flow and central voucher system
- VoucherService: central voucher register with globally unique voucher numbers
- CashierDepositRequestApiResource: full submit/approve/reject/list flow
  - Cashier submits deposit request; balance credited only on manager approval
  - Self-approval blocked (maker cannot be checker)
  - Role guard: only manager/head-cashier/ALL_FUNCTIONS can approve/reject
  - Race condition guard: WHERE status='PENDING' on approve/reject UPDATE
  - Clean JSON error responses (no 500 stack traces) via apiError() helper
  - Denomination total validation at submit and approve
- ClerkWithdrawalRequestApiResource: refactored to use m_voucher
  - Voucher created at submit, linked to savings txn at approve
  - Same role guard and self-approval block as deposit flow
  - Disburse: denomination validation, null denoms for non-cash payments
- Migrations 0327-0330:
  - 0327: add voucher_no to m_clerk_withdrawal_request
  - 0328: add voucher_no to m_savings_account_transaction
  - 0329: create m_voucher central table (globally unique voucher_no)
  - 0330: create m_cashier_deposit_request table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 10:53:47 +05:30
Jaanu423andClaude Sonnet 4.6 8d5b4d1ce9 feat(clerk-withdrawal): add maker-checker withdrawal disbursement API
- ClerkWithdrawalRequestApiResource: full CRUD for withdrawal requests
- Endpoints: submit, list, myRequests, getById, getByToken, approve, reject, disburse, update
- Token uniqueness validated per day (validateTokenUniqueToday)
- Cashier disburse: amount match, race condition guard (WHERE status=APPROVED)
- Disbursement denomination saved to m_savings_txn_denomination for vault position tracking
- Liquibase migrations: 0321-0326 (create table, tok_no, manager fields, disbursement fields, drop unused counter)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 16:00:57 +05:30
Jaanu423andClaude Sonnet 4.6 db94056144 feat(cashier): add server-side pagination and date ordering to GL entries endpoint
- Add offset/limit query params with defaults (limit=20, offset=0)
- Return totalFilteredRecords + pageItems response structure
- Compute totalDr/totalCr from all records (not just current page)
- Fix ordering: ORDER BY MAX(created_at) for correct chronological sort

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:53:49 +05:30
Jaanu423andClaude Sonnet 4.6 19639c9e02 feat(cashier): add GL denomination tracking, live GL balance, and enhanced closing report
- Add CashierGlDenominationApiResource: new POST /v1/cashier/gl-denomination endpoint
  to record denomination breakdown for GL journal entries in m_savings_txn_denomination
- Fix GL account real-time balance in GLAccountReadPlatformServiceImpl using live SUM query
- Add Liquibase migrations 0318/0319/0320: make savings_account_id and txn_id nullable,
  add payment_type column to m_savings_txn_denomination
- Add validCheckSum to 0317b changeset to resolve checksum mismatch on existing installs
- Enhance GL entries closing report SQL with date, time, teller, payment type, txn source

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:31:12 +05:30
Jaanu423andClaude Sonnet 4.6 53b6a94bf8 fix(cashier): fix GL account search SQL bug, running balance, and add CASHIER role permissions
- Fix searchParam SQL syntax: parameterized LIKE with %value% instead of literal %?%
- Fix organizationRunningBalance: correlated subquery for latest journal entry (ORDER BY id DESC LIMIT 1)
- Migration 0317: grant READ_GLACCOUNT, READ_LOAN, READ_SHAREACCOUNT to CASHIER role

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 11:17:32 +05:30
Jaanu423andClaude Sonnet 4.6 6d0c1fe7f6 feat(clerk): add clerk role with cashier teller workstation permissions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 18:27:04 +05:30
Jaanu423andClaude Sonnet 4.6 edaeb674d5 feat(cashier): fix current account transactions search and withdrawal validations
CurrentAccountTransactionsApiResource:
- Fixed: search endpoint now queries m_current_account_transaction
  instead of m_savings_account_transaction (wrong table)
- Added remarks column to search query (column exists in CA table)
- Charge Deduction (type=7) mapped in transactionType value
- Removed sat.remarks that caused 500 (column doesn't exist in savings table)

CurrentAccountTransactionWritePlatformServiceImpl:
- Added min required balance validation in withdrawal()
- Throws GeneralPlatformDomainRuleException when balance after withdrawal
  would fall below minRequiredBalance (if enforceMinRequiredBalance=true)

StandingInstructionReadPlatformServiceImpl:
- Fixed SQL WHERE clause: fromAccountType + fromAccount condition
  now correctly adds WHERE when fromAccountId is set

Liquibase 0315:
- Fix standing instruction permission code (trim trailing space on id=449)
- Grant READ permissions to CASHIER role for transaction entry screen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 22:27:59 +05:30
Jaanu423andClaude Sonnet 4.6 2371ad98b1 feat(cashier): add role-based data filtering and pagination for teller workstation
- Add CashierRoleHelper service: detects CASHIER role via DB permission
  check, resolves cashier ID from staff/user linkage
- Role-based filtering on GET list endpoints (cashtransfer, cashsettlement,
  cashintercashier, cashexchange, cashreports): CASHIER sees only own data,
  ADMIN/HEAD sees all
- Pagination (offset/limit) added to all cashier list APIs returning
  standard Fineract Page format {totalFilteredRecords, pageItems}
- ShareCollectionApiResource: collect cash before account activation,
  post GL entries (Dr Cash Vault / Cr Share Sundry / Cr Income from Fees),
  mark charges paid to prevent duplicate GL on activation,
  GET status endpoint for collection check
- Migration 0315: grant READ_CLIENTKYC, READ_CLIENTNOMINEE, READ_ADDRESS,
  READ_FAMILYMEMBERS, READ_SAVINGSACCOUNT to CASHIER role

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:00:55 +05:30
Jaanu423andClaude Sonnet 4.6 9d944d0196 feat(share): add cashier share collection endpoint with GL posting and activation validation
- New POST /cashier/sharecollection endpoint: collects cash for share purchase,
  posts Dr Cash Vault / Cr Share Sundry / Cr Income GL entries, marks charges
  as paid and sets cashier_collected flag on the account
- GET /cashier/sharecollection/:id endpoint for collection status check
- Block share account activation if cashier has not collected cash first
  (validates m_share_cashier_collection record within last 30 days)
- Skip duplicate charge GL on activation when charge already collected upfront
- Suppress automatic GL posting at account submission time to prevent
  double entries; GL is now posted only via cashier collection endpoint
- Fix JdbcTemplate injection in ShareAccountWritePlatformServiceJpaRepositoryImpl
  via ShareAccountsConfiguration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 13:22:33 +05:30
Jaanu423andClaude Sonnet 4.6 6e31ac9acd fix(savings): allow modifyApplication for rejected Savings/FD/RD accounts
SavingsAccount.modifyApplication() previously blocked modification unless
the account was in SUBMITTED_AND_PENDING_APPROVAL state. Extended to also
allow REJECTED state so bank users can amend and re-submit rejected applications.

When a rejected account is modified, status resets to
SUBMITTED_AND_PENDING_APPROVAL (rejectedOnDate and rejectedBy cleared)
so the account re-enters the approval workflow.

Applies to Savings, FD, and RD accounts (all extend SavingsAccount).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 14:55:39 +05:30
Jaanu423andClaude Sonnet 4.6 a051faba5d feat(accounts): backend fixes for Modify Application feature (Current/Savings/FD/RD)
Current Account:
- CurrentAccountReadPlatformServiceImpl: add retrieveJointHolders() and retrievePartners()
- CurrentAccountWritePlatformServiceImpl: full modifyApplication() - updates all CBS fields,
  joint holders, partners, nominees, facility config, dates, introducer
- Fix cbsAccountType save: parse string account type from 'accountType' JSON field

Nominees:
- ClientNomineeWritePlatformService: add replaceNomineesForAccount() for atomic replace
- Savings/FD/RD modifyApplication: replace nominees on modify

FD/RD Read (DepositAccountReadPlatformServiceImpl):
- Add isSeniorCitizen, seniorCitizenInterestRate to BASE SQL SELECT
- Add setCbsAccountFields() call in FD/RD mappers reading accountStatus, modeOfOperation,
  introducerClientId, guardianFields, linkedAccountId, isSeniorCitizen from ResultSet
- Fix retrieveOne() to also fetch joint holders from m_savings_account_joint_holders
- Use explicit LinkedHashMap in joint holder query (camelCase keys for Angular)

FD/RD Data Classes:
- FixedDepositAccountData: add accountStatus, modeOfOperation, introducerClientId,
  linkedAccountId, jointHolders, isSeniorCitizen, seniorCitizenInterestRate fields
- RecurringDepositAccountData: same fields added
- Fix all factory methods (withInterestChartAndRecurringDetails, withClosureTemplateDetails,
  withTemplateOptions, associationsAndTemplate) to call copyCbsFieldsFrom()
- Fix SavingsAccountReadPlatformServiceImpl joint holders: use camelCase keys

FD Calculator:
- FixedDepositAccountInterestCalculationServiceImpl: add calculateExact() with actual
  calendar-based calculation matching PostingPeriod engine (exact same as account creation)
- Add submittedOnDate, daysInYear, interestPostingPeriodInMonths optional params
- Support interestCompoundingPeriodInMonths=0 (daily compounding) in both validator and calc
- DepositsApiConstants: add submittedOnDate/dateFormat/locale/daysInYear to allowed params
- DepositAccountDataValidator: allow 0 (daily) in interestCompoundingPeriodInMonths

Misc:
- SavingsProductData: null-check in isCashBased/isAccrualBased methods (NPE fix)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 10:32:59 +05:30
Jaanu423andClaude Sonnet 4.6 7c4cf6260c feat(cashier): GL entries endpoint + remove vault-cashier GL postings
- GET /cashreports/gl-entries: returns voucher_no, ac_no, ac_name
  (customer name), dr (withdrawal), cr (deposit) for cashier transactions
  with optional cashierId/tellerId filter
- TellerWritePlatformServiceJpaImpl: removed ALLOCATE/SETTLE GL entries
- OrganisationTellerConfiguration: removed obsolete GL constructor args

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 10:47:22 +05:30
Jaanu423andClaude Sonnet 4.6 6bdcac7b77 feat(cashier): remove GL entries for vault-to-cashier transfers (Option A)
ALLOCATE (vault→cashier) and SETTLE (cashier→vault) no longer post
DR CASH_AT_TELLER / CR CASH_AT_MAINVAULT journal entries.

Rationale: Cash transfer between vault and teller drawer is an internal
physical movement — the denomination/amount is already tracked operationally
via m_cash_transfer and m_cash_settlement tables. A single Cash in Hand GL
(CASH_AT_MAINVAULT) covers all bank cash. GL entries now only occur on
actual customer deposit/withdrawal transactions (SAVINGS_REFERENCE ↔
SAVINGS_CONTROL), keeping the GL clean and RBI-compliant.

Removed unused imports and field injections:
  JournalEntryRepository, FinancialActivityAccountRepositoryWrapper,
  FinancialActivity, FinancialActivityAccount, GLAccount,
  JournalEntry, JournalEntryType

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 07:13:48 +05:30
Jaanu423andClaude Sonnet 4.6 8bf28a8033 fix(cashier): prevent duplicate cashier lookup crash + add missing migrations
- DenominationBreakdownWriter: add ORDER BY mc.id DESC LIMIT 1 to all cashier
  lookup queries so multiple cashier records per staff do not crash queryForObject
- CashReportApiResource: same LIMIT 1 fix for cashier-day-status and position endpoints
- 0311: fix m_client and m_savings_account NOT NULL columns (created_by, last_modified_by,
  created_on_utc, last_modified_on_utc, cif_number, is_lien_allowed, etc.)
- 0312: create CASHIER role if missing + grant all 34 cashier permissions (incl FD/RD)
- 0313: add missing m_cash_exchange columns (cashier_id, is_reversed, reversed_by,
  reversed_at, reverse_reason, customer_id_proof)
- 0314: unique constraint on m_cashiers.staff_id to prevent duplicate allocations
- application.properties: restore local postgres password after merge
- ExecuteOverdueAndCurrentStandingInstructionsTest: fix constructor call (5 args)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 18:23:31 +05:30
Jaanu423 d4262ebf6f Merge remote-tracking branch 'origin/feature/switch-integration-backend' into feature/account-charges 2026-06-01 13:23:22 +05:30
Jaanu423andClaude Sonnet 4.6 656a0b4f13 feat(cashier): add morning transfer validation & fix denomination SQL
- validateCashierHasMorningTransfer(): blocks all txns when no today's cash transfer in m_cash_transfer
- validatePreviousDayClosed(): blocks when previous business day unsettled
- Both validations called on deposit AND withdrawal in Savings, Current, FD, RD services
- cashier-day-status endpoint now returns morningTransferReceived field
- getCashierTotalCash SQL fixed: dm.denomination_value -> dm.denomination

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 13:15:52 +05:30
Aditya ae7017ca81 @
feat(fundtransfer+imps+mfa): fund transfer module, IMPS switch client, MFA log API

- Fund transfer: NEFT/RTGS/IMPS/internal strategies, limit resolution, bulk
  transfer, verification tokens, suspense GL, recall/approval flow, jobs
- IMPS CBS switch client with crypto (JWT/envelope) and transaction domain
- MFA log read API (audit of MFA events)
- Liquibase migrations 0300-0311 for fund transfer / IMPS tables, permissions,
  roles, and suspense account
- JobName, SecurityConfig, dependencies.gradle wiring

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-01 12:18:30 +05:30
Aditya 6413befe07 Merge self-hosted/feature/account-charges into feature/Backend-DSC-MFA 2026-06-01 12:14:49 +05:30
Jaanu423andClaude Sonnet 4.6 eae5b03d7e feat(cashier): denomination withdrawal validation, exchange detail fix, cash exchange in excess report
- Savings/Current/FD/RD withdrawal services: call validateWithdrawalStock() before
  validateBreakdown() to block withdrawal when cashier lacks the requested denomination
- CashExchangeApiResource: validate cashier denomination stock before giving to customer;
  fix saveAndFlush (EclipseLink does not flush on save() — getId() returned null causing
  exchange_id NOT NULL violation in m_cash_exchange_detail)
- CashReportApiResource excess(): include cash exchanges >= threshold in result,
  add type field (SAVINGS_TXN/EXCHANGE) and idProof for RBI/AML compliance;
  combined list sorted by amount DESC
- CashReportApiResource closing(): add denomination-wise exchange rows (already present)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 10:11:44 +05:30
Jaanu423andClaude Sonnet 4.6 49d64004cc feat(cashier): add cashier-morning balance endpoint and expose fromCashierId in settlement/transfer APIs
- GET /cashreports/cashier-morning: returns denomination-wise cash received by cashier from vault
  for a given date (only vault→cashier transfers, non-reversed)
- CashSettlementApiResource: include fromCashierId in list and detail responses
- CashTransferApiResource: include toCashierId in list and detail responses for traceability

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 08:55:09 +05:30
Jaanu423 ada24d7497 feat(cashier): implement expected balance for logged-in cashier, settlement checks, and reconciliation rules 2026-05-28 22:28:30 +05:30
Jaanu423andClaude Sonnet 4.6 b89e653bcb feat: cash management backend - transfer, settlement, inter-cashier, reconciliation + cashier /me position endpoint
OVERVIEW
--------
Added complete cash management backend covering the full cashier workflow:
vault transfers, inter-cashier transfers, end-of-day settlement, GL
reconciliation, and a new authenticated /me endpoint for cashier position
lookup without needing to pass a cashier ID.

NEW API ENDPOINT
----------------
GET /cashreports/cashier-position/me
- Auto-detects the authenticated user via PlatformSecurityContext
- Resolves appUser -> staff -> cashier via m_cashiers JOIN chain
- Accepts optional ?asOfDate=YYYY-MM-DD query param (defaults to today)
- Reuses existing buildPositionForCashier(cashierId, dateFilter) internally
- Returns: { cashierId, asOfDate, totalAmount, denominations[] }
- Non-cashier users get empty denominations array (no error thrown)
- Used by Angular UI to show opening/closing denomination panels

CASH TRANSFER (vault -> cashier)
---------------------------------
CashTransferApiResource.java   — REST endpoints (POST /cashtransfer, GET list/detail)
CashTransfer.java              — JPA entity (m_cash_transfer)
CashTransferDetail.java        — JPA entity (denomination breakdown, m_cash_transfer_detail)
CashTransferRepository.java    — Spring Data JPA repository
CashTransferDetailRepository.java

DB migrations:
  0014_cash_transfer.xml       — creates m_cash_transfer + m_cash_transfer_detail tables
  0015_cash_transfer_add_ids.xml — adds missing ID columns to transfer detail

CASH SETTLEMENT (end-of-day cashier close)
------------------------------------------
CashSettlementApiResource.java — REST endpoints (POST /cashsettlement, GET list/detail)
CashSettlement.java            — JPA entity (m_cash_settlement)
CashSettlementDetail.java      — JPA entity (denomination breakdown)
CashSettlementRepository.java
CashSettlementDetailRepository.java

DB migration:
  0016_cash_settlement.xml     — creates m_cash_settlement + m_cash_settlement_detail

INTER-CASHIER TRANSFER
-----------------------
CashInterCashierApiResource.java — REST endpoints (POST /cashintercashier, GET list/detail)
CashInterCashier.java            — JPA entity (m_cash_inter_cashier)
CashInterCashierDetail.java      — JPA entity (denomination breakdown)
CashInterCashierRepository.java
CashInterCashierDetailRepository.java

DB migration:
  0017_cash_inter_cashier.xml  — creates m_cash_inter_cashier + detail table

CASH RECONCILIATION
--------------------
CashReconciliationApiResource.java — REST endpoints (POST /cashreconciliation, GET list)
CashReconciliation.java            — JPA entity (m_cash_reconciliation)
CashReconciliationDetail.java      — JPA entity (denomination breakdown)
CashReconciliationRepository.java
CashReconciliationDetailRepository.java

DB migrations:
  0018_cash_reconciliation.xml — creates m_cash_reconciliation + detail table
  0019_cash_reversal_columns.xml — adds reversal tracking columns
  0020_cash_permissions.xml    — adds permissions for all cash management APIs

GL DISPATCH SERVICE
-------------------
GlDispatchService.java (portfolio/teller/service/)
- Shared service for posting GL journal entries for cash movements
- Used by settlement and reconciliation flows

CHANGELOG
---------
changelog-tenant.xml updated to include migrations 0014 through 0020.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 17:40:57 +05:30
Jaanu423andClaude Opus 4.7 9a30ede278 feat: denomination save+validation for all deposit types + cash exchange backend
- DenominationBreakdownWriter helper: validates breakdown total == amount and
  persists per-denomination rows; wired into current, FD and RD deposit flows
  (savings already had inline save) so all 4 deposit types now record the
  cash denomination breakdown.
- CIF on denomination rows now sourced from the client (getCifNumber) instead
  of the account external id.
- Allow denominationBreakdown param in the FD/RD transaction validator.
- Cash Exchange backend: m_cash_exchange + m_cash_exchange_detail tables,
  entities, repositories and JAX-RS API with received==given validation;
  registered the 0013 migration in changelog-tenant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:36:00 +05:30
Jaanu423andClaude Opus 4.7 ee915e395a feat: Denomination Master backend - master, audit, soft delete, txn breakdown
- m_denomination_master CRUD API with duplicate (denomination+kind)
  validation, soft delete + restore, and a full audit trail
  (m_denomination_master_audit logs CREATE/UPDATE/ACTIVATE/DEACTIVATE/
  DELETE/RESTORE with snapshot, changes, user and timestamp)
- m_savings_txn_denomination: persist per-transaction cash denomination
  breakdown on savings deposit; allow denominationBreakdown request param
- Liquibase 0012_denomination_master registered in tenant changelog

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:16:52 +05:30
Jaanu423andClaude Sonnet 4.6 7d4f409202 feat: fix standing instructions GL accounting for current account transfers
- AccountTransfersWritePlatformServiceImpl:
  * Direct GL posting for current↔savings/current transfers:
    DR debit-side account GL (with correct account entity_id)
    CR credit-side account GL (with correct account entity_id)
  * Prefix current account txn_id with CA (not C) so
    JournalEntryReadPlatformServiceImpl joins m_current_account
    and shows correct current account number on GL entries
  * Suppress redundant savings deposit/withdrawal GL entries
    (skipAccountingPostings=true) when current account handles
    the full double-entry directly
  * Separate performCurrentWithdrawalForLoan for Current→Loan
    transfers (loan domain service handles the other GL leg)
  * Reuse existing AccountTransferDetails for SI recurring runs
    so transactions appear in list-account-transactions history

- SavingsAccountDomainService / SavingsAccountDomainServiceJpa:
  * New handleDeposit overload with skipAccountingPostings flag
  * handleWithdrawal respects SavingsTransactionBooleanValues
    .isSkipAccountingPostings() to suppress redundant GL entries

- SavingsTransactionBooleanValues:
  * Added skipAccountingPostings field + overloaded constructor

- AccountingProcessorHelper:
  * New createDirectTransferJournalEntriesForSavings() method
    creates DR and CR entries with separate productId/entityId
    so each GL line shows the correct account number

- JournalEntryReadPlatformServiceImpl:
  * Join both m_savings_account and m_current_account always
  * COALESCE(ca.account_no, sa.account_no) to resolve correct
    account number regardless of transaction_id prefix

- ExecuteStandingInstructionsTasklet:
  * Load existing AccountTransferDetails via repository so
    subsequent SI runs link to the same transfer detail record

- ExecuteStandingInstructionsConfig / AccountConfiguration:
  * Wire AccountTransferDetailRepository and
    AccountingProcessorHelper into the relevant beans

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 16:30:04 +05:30
Jaanu423andClaude Sonnet 4.6 2251fd2259 feat: current account standing instructions - full backend support
- StandingInstructionData: rename conflicting Lombok getters (getInstructionType,
  getRecurrenceType, getRecurrenceFrequency, getFromAccountType, getToAccountType,
  getTransferType) to domain enum methods so Jackson serializes EnumOptionData fields
  as {id, code, value} objects instead of plain strings
- AccountTransfersWritePlatformServiceImpl: add current account transfer support for
  all 4 missing cases (Current->Savings, Savings->Current, Current->Current,
  Current->Loan) with performCurrentWithdrawal/performCurrentDeposit helpers
- AccountConfiguration: wire CurrentAccountRepository and CurrentAccountTransactionRepository
  into AccountTransfersWritePlatformService bean
- StandingInstructionApiResource: fix getValue() type mismatch after getter rename
- ExecuteStandingInstructionsTasklet: update to use renamed enum accessor methods
- PortfolioAccountType: add CURRENT(3) type support
- DB migration: 0301_add_current_account_to_transfer_details.xml adds
  from_current_account_id and to_current_account_id columns to transfer details table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 10:55:35 +05:30
AdityaandClaude Opus 4.7 45585ecc10 Fix family-member PUT 500; CORS POST 403; Hikari local password default
- ClientFamilyMembersWritePlatformServiceImpl#updateFamilyMember: guard
  maritalStatusId/genderId/professionId with != null instead of != 0 so a
  payload that omits these keys doesn't NPE when the boxed Long is unboxed.
- application.properties: fix the broken property name
  `fineract.security.cors.allowed-metho pas` -> `allowed-methods`. The typo
  left CORS allowed-methods empty, which made Spring reject every browser
  POST with "Invalid CORS request" 403 once the SPA sent an Origin header.
- application.properties: change the Hikari default password from 313863
  to root so the local devRun matches the tenant-DB default at line 49.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:07:24 +05:30
AdityaandClaude Opus 4.7 7dea88d30f Fix family-member PUT 500; CORS POST 403; Hikari local password default
- ClientFamilyMembersWritePlatformServiceImpl#updateFamilyMember: guard
  maritalStatusId/genderId/professionId with != null instead of != 0 so a
  payload that omits these keys doesn't NPE when the boxed Long is unboxed.
- application.properties: fix the broken property name
  `fineract.security.cors.allowed-metho pas` -> `allowed-methods`. The typo
  left CORS allowed-methods empty, which made Spring reject every browser
  POST with "Invalid CORS request" 403 once the SPA sent an Origin header.
- application.properties: change the Hikari default password from 313863
  to root so the local devRun matches the tenant-DB default at line 49.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:06:38 +05:30
Jaanu423 d32953ed76 Merge branch 'feature/account-charges' of https://git.zeptic.in/CBSNexora/JAVA-Backend-CBS-MAIN into feature/account-charges 2026-05-21 21:00:05 +05:30
AdityaandClaude Opus 4.7 5a98a89839 KYC: PAN entity-branch, defensive LincPay parse, family/nominee rules for non-individuals
PAN verify for non-individual clients was sending name="" + dob=null to LincPay because
the code path always read firstname/middlename/lastname (NULL on entities) and
client.dateOfBirth (also NULL on entities). LincPay then returned plain-text "Invalid
DOB format. Required format is dd/MM/yyyy", which Gson can't parse (MalformedJsonException
at line 1 col 10), surfacing as a useless 503. Now ClientKycWritePlatformServiceJpa-
RepositoryImpl.verifyPan branches on LegalForm:
  - PERSON  → name = firstname + middlename + lastname,    dob = client.dateOfBirth
  - ENTITY  → name = client.fullname,                       dob = clientNonPerson.dateOfRegistration
Injected ClientNonPersonRepositoryWrapper for the entity branch.

ClientNonPerson got two missing getters (getConstitution, getDateOfRegistration) needed
by the above plus the family/nominee rules below.

Defensive JSON parse in LincPayKycVerificationServiceImpl: parseJson now returns null
when the body doesn't even start with '{' or '[' (i.e. vendor sent plain text or HTML),
logs the literal body at WARN, and every verify method (PAN, GSTIN, Aadhaar OTP gen,
Aadhaar OTP verify, CIN, DIN) checks for null and returns
  "<X> verification failed — vendor returned non-JSON: <first 200 chars>"
So future vendor outages surface the actual response text instead of a MalformedJson-
Exception with no context.

Non-individual client rules:
- ClientFamilyMembersWritePlatformService: addFamilyMember / addClientFamilyMember /
  updateFamilyMember now refuse with PlatformApiDataValidationException when the client
  is an entity. Family members are an individual-only concept. Delete is intentionally
  unguarded so legacy rows can be cleaned up.
- ClientNomineeWritePlatformService: addNominee / updateNominee refuse for non-individual
  clients UNLESS the constitution normalises to PROPRIETORSHIP (matches both
  "PROPRIETOR SHIP" and "PROPRIETORSHIP"). Proprietorships are operationally individuals
  with a separate trade name, so they keep nominee support; companies / LLPs / trusts /
  societies / HUFs / partnerships don't carry a nomination concept. Delete unguarded.

Verified the LincPay endpoints directly:
  /validate-pan   AQRPV8345E + "Aditya Vyas" + 19/12/1988      → valid (individual)
  /validate-pan   AAMCG5406B + "GIZMOGLADE..." + 01/12/2025    → valid (company)
  /validate-pan   AAMCG5406B + ""             + ""             → "Invalid DOB format" plain text (the failing path)
  /validate-gstn  23AADCL9984A1ZH                              → 200 with full company data
  /validate-gstn  21AAMCG5406BIZ6                              → 400 "Invalid GSTIN pattern" (bad checksum, not our bug)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:46:34 +05:30
Aditya d5157ef701 Merge remote-tracking branch 'self-hosted/feature/account-charges' into feature/Backend-DSC-MFA 2026-05-21 16:15:28 +05:30
AdityaandClaude Opus 4.7 c6f130e116 Fix BadSqlGrammarException on GET /clients?status=pending — missing SELECT column
The second schema() builder used by the clients-list query had the JOIN
  left join m_client gc on gc.id = c.guardian_client_id
and the SELECT
  c.guardian_client_id as guardianClientId
but was missing
  gc.display_name as guardianClientDisplayName
which the RowMapper still reads (rs.getString("guardianClientDisplayName")).

Result: PSQLException "The column name guardianClientDisplayName was not found
in this ResultSet" → BadSqlGrammarException → 500 on any client-list pull.

The matching first builder (~line 327) had both columns correctly. Added the
missing SELECT line to keep the pair consistent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:35:59 +05:30
1124 changed files with 111741 additions and 384232 deletions
+1 -1
View File
@@ -1 +1 @@
{"sessionId":"0fda6e48-a79d-4980-a9ee-31a6d5f735e1","pid":15548,"acquiredAt":1776168814856} {"sessionId":"b5222d29-c337-4c5d-afde-70cde98cdf46","pid":426448,"acquiredAt":1780052566378}
+26 -1
View File
@@ -52,7 +52,32 @@
"Bash(git -c commit.gpgsign=false commit -m ' *)", "Bash(git -c commit.gpgsign=false commit -m ' *)",
"Bash(git pull *)", "Bash(git pull *)",
"Bash(./gradlew :fineract-security:compileJava :fineract-provider:compileJava -q)", "Bash(./gradlew :fineract-security:compileJava :fineract-provider:compileJava -q)",
"Bash(git ls-remote *)" "Bash(git ls-remote *)",
"Bash(PGPASSWORD=root psql -h localhost -p 5432 -U postgres -d fineract_default -c \"SELECT cv.id, cv.code_value FROM m_code_value cv JOIN m_code c ON cv.code_id=c.id WHERE c.code_name='Constitution' ORDER BY cv.id;\")",
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes | %{time_total}s ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-pan -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"panNumber\":\"AQRPV8345E\",\"name\":\"Aditya Vyas\",\"dob\":\"19/12/1988\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes | %{time_total}s ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-pan -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"panNumber\":\"AAMCG5406B\",\"name\":\"GIZMOGLADE PRIVATE LIMITED\",\"dob\":\"01/12/2025\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-pan -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"panNumber\":\"AAMCG5406B\",\"name\":\"\",\"dob\":\"\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-gstn -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"gstNumber\":\"21AAMCG5406BIZ6\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
"Bash(curl -s -w '\\\\n--- HTTP %{http_code} | content-type: %{content_type} | %{size_download} bytes | %{time_total}s ---\\\\n' -X POST https://ekyc.lincpay.in/EKYC/ekyc/validate-gstn -H 'Content-Type: application/json' -d '{\"apikey\":\"api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f\",\"gstNumber\":\"23AADCL9984A1ZH\",\"secrete\":\"secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d\"}')",
"PowerShell(Get-Process | Where-Object { $_.ProcessName -match '^\\(java|gradle\\)' } | Select-Object Id, ProcessName, StartTime, @{n='CPU\\(s\\)';e={[math]::Round\\($_.CPU,1\\)}}, @{n='WS\\(MB\\)';e={[math]::Round\\($_.WorkingSet64/1MB,0\\)}} | Format-Table -AutoSize)",
"PowerShell(Get-NetTCPConnection -LocalPort 8443 -ErrorAction SilentlyContinue)",
"PowerShell(Stop-Process -Id 3468,3864,39964 -Force)",
"PowerShell(Get-Process -Id 3468,3864,39964 -ErrorAction SilentlyContinue)",
"PowerShell(foreach \\($id in 3468,3864,39964\\) { try { Stop-Process -Id $id -Force -ErrorAction Stop; \"killed $id\" } catch { \"skip $id - $\\($_.Exception.Message\\)\" } })",
"PowerShell(Get-Process java -ErrorAction SilentlyContinue)",
"PowerShell(if \\(-not \\(Test-Path .\\\\fineract-provider\\\\logs\\)\\) { New-Item -ItemType Directory -Force .\\\\fineract-provider\\\\logs | Out-Null }; Remove-Item .\\\\fineract-provider\\\\logs\\\\devrun.out -ErrorAction SilentlyContinue; \"ready\")",
"PowerShell($j = Get-Process java -ErrorAction SilentlyContinue | Select-Object Id, @{n='CPU\\(s\\)';e={[math]::Round\\($_.CPU,1\\)}}, @{n='WS\\(MB\\)';e={[math]::Round\\($_.WorkingSet64/1MB,0\\)}}, StartTime; $j | Format-Table -AutoSize; \"---\"; Get-NetTCPConnection -LocalPort 8443 -State Listen -ErrorAction SilentlyContinue | Select-Object LocalAddress, OwningProcess)",
"PowerShell(if \\(Test-Path .\\\\fineract-provider\\\\logs\\\\devrun.out\\) { $f = Get-Item .\\\\fineract-provider\\\\logs\\\\devrun.out; \"size=$\\($f.Length\\) bytes mtime=$\\($f.LastWriteTime\\)\" } else { \"devrun.out missing\" })",
"PowerShell(netstat -ano)",
"PowerShell(Get-Process java -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Milliseconds 800; $left = Get-Process java -ErrorAction SilentlyContinue; if \\($left\\) { $left | Select-Object Id, ProcessName } else { \"no java processes\" }; Get-NetTCPConnection -LocalPort 8443 -State Listen -ErrorAction SilentlyContinue | Measure-Object | ForEach-Object { \"8443 listeners: $\\($_.Count\\)\" })",
"Bash(curl -sk -o /tmp/resp.txt -w \"STATUS=%{http_code}\\\\nCONTENT_TYPE=%{content_type}\\\\n\" -X POST 'https://localhost:8443/fineract-provider/api/v1/authentication' -H 'Content-Type: application/json' -H 'Fineract-Platform-TenantId: default' -d '{\"username\":\"mifos\",\"password\":\"password\"}')",
"Bash(curl -sk -o /tmp/r1.txt -w \"STATUS=%{http_code}\\\\n\" -X POST 'https://localhost:8443/fineract-provider/api/v1/authentication' -H 'Content-Type: application/json' -d '{\"username\":\"mifos\",\"password\":\"password\"}')",
"Bash(curl -s -o /tmp/r2.txt -w \"STATUS=%{http_code}\\\\n\" -X POST 'http://localhost:4200/fineract-provider/api/v1/authentication' -H 'Content-Type: application/json' -d '{\"username\":\"mifos\",\"password\":\"password\"}')",
"Bash(curl -s -o /tmp/r3.txt -w \"STATUS=%{http_code}\\\\n\" -X POST 'http://localhost:4200/fineract-provider/api/v1/authentication' -H 'Content-Type: application/json' -H 'Origin: http://localhost:4200' -H 'Fineract-Platform-TenantId: default' -d '{\"username\":\"mifos\",\"password\":\"password\"}')",
"Bash(curl -s -o /tmp/r4.txt -w \"STATUS=%{http_code}\\\\n\" -X OPTIONS 'http://localhost:4200/fineract-provider/api/v1/authentication' -H 'Origin: http://localhost:4200' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: content-type,fineract-platform-tenantid' -i)",
"Bash(git cherry-pick *)",
"PowerShell($parent = 'C:\\\\Users\\\\adity\\\\Music\\\\CBS'; if \\(Test-Path $parent\\) { \"parent exists: $parent\"; Get-ChildItem $parent -Directory | Select-Object -ExpandProperty Name } else { \"MISSING parent: $parent\" }; \"---\"; $target = Join-Path $parent 'LincpayEKYC'; if \\(Test-Path $target\\) { \"TARGET ALREADY EXISTS: $target\" } else { \"target free: $target\" })",
"Bash(git clone *)"
], ],
"additionalDirectories": [ "additionalDirectories": [
"c:\\Users\\adity\\Music\\CBS\\cfcfrms-bank-api", "c:\\Users\\adity\\Music\\CBS\\cfcfrms-bank-api",
+3
View File
@@ -35,3 +35,6 @@ gradleExp/
.java-version .java-version
.windsurf/ .windsurf/
# Application logs
fineract-provider/logs/
+1
View File
@@ -0,0 +1 @@
{"username":"mifos", "password":"password"}
+18
View File
@@ -0,0 +1,18 @@
const https = require('https');
const agent = new https.Agent({ rejectUnauthorized: false });
fetch('https://localhost:8443/fineract-provider/api/v1/authentication?username=mifos&password=password&tenantIdentifier=default', {
method: 'POST',
agent: agent
}).then(res => res.json()).then(data => {
const token = data.base64EncodedAuthenticationKey;
return fetch('https://localhost:8443/fineract-provider/api/v1/clients/9/accounts?tenantIdentifier=default', {
headers: {
'Authorization': 'Basic ' + token,
'Fineract-Platform-TenantId': 'default'
},
agent: agent
});
}).then(res => res.json()).then(data => {
console.log(JSON.stringify(data, null, 2));
}).catch(console.error);
@@ -74,7 +74,10 @@ public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformSe
} }
sb.append("from acc_gl_account gl left join m_code_value cv on tag_id=cv.id "); sb.append("from acc_gl_account gl left join m_code_value cv on tag_id=cv.id ");
if (this.associationParametersData.isRunningBalanceRequired()) { if (this.associationParametersData.isRunningBalanceRequired()) {
sb.append("left outer Join acc_gl_journal_entry gl_j on gl_j.account_id = gl.id"); sb.append("left outer join acc_gl_journal_entry gl_j on gl_j.id = (")
.append("select id from acc_gl_journal_entry ")
.append("where account_id = gl.id ")
.append("order by id desc limit 1) ");
} }
return sb.toString(); return sb.toString();
} }
@@ -149,10 +152,10 @@ public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformSe
if (firstWhereConditionAdded) { if (firstWhereConditionAdded) {
sql += SQL_AND; sql += SQL_AND;
} }
sql += " ( name like %?% or gl_code like %?% )"; sql += " ( name like ? or gl_code like ? )";
parameterArray[arrayPos] = searchParam; parameterArray[arrayPos] = "%" + searchParam + "%";
arrayPos = arrayPos + 1; arrayPos = arrayPos + 1;
parameterArray[arrayPos] = searchParam; parameterArray[arrayPos] = "%" + searchParam + "%";
arrayPos = arrayPos + 1; arrayPos = arrayPos + 1;
firstWhereConditionAdded = true; firstWhereConditionAdded = true;
} }
@@ -197,12 +200,21 @@ public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformSe
final StringBuilder sql = new StringBuilder(); final StringBuilder sql = new StringBuilder();
sql.append("select ").append(rm.schema()); sql.append("select ").append(rm.schema());
sql.append(" where gl.id = ?"); sql.append(" where gl.id = ?");
if (associationParametersData.isRunningBalanceRequired()) {
sql.append(" and gl_j.is_running_balance_calculated = true ") final GLAccountData glAccount = this.jdbcTemplate.queryForObject(sql.toString(), rm, new Object[] { glAccountId });
.append(" ORDER BY gl_j.entry_date DESC,gl_j.id DESC LIMIT 1");
// Live balance: sum all non-reversed journal entries so cashier sees real-time balance
// without waiting for the "Update Accounting Running Balances" scheduled job.
if (associationParametersData != null && associationParametersData.isRunningBalanceRequired() && glAccount != null) {
final String liveBalanceSql = "SELECT COALESCE(ABS(SUM(CASE WHEN type_enum = 2 THEN amount ELSE -amount END)), 0) "
+ "FROM acc_gl_journal_entry "
+ "WHERE account_id = ? AND (reversed = false OR reversed IS NULL)";
final java.math.BigDecimal liveBalance = this.jdbcTemplate.queryForObject(liveBalanceSql,
java.math.BigDecimal.class, glAccountId);
glAccount.setOrganizationRunningBalance(liveBalance != null ? liveBalance.longValue() : 0L);
} }
return this.jdbcTemplate.queryForObject(sql.toString(), rm, new Object[] { glAccountId }); return glAccount;
} catch (final EmptyResultDataAccessException e) { } catch (final EmptyResultDataAccessException e) {
throw new GLAccountNotFoundException(glAccountId, e); throw new GLAccountNotFoundException(glAccountId, e);
} }
@@ -18,7 +18,6 @@
*/ */
package org.apache.fineract.accounting.journalentry; package org.apache.fineract.accounting.journalentry;
import java.time.OffsetDateTime;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType; import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.journalentry.data.JournalEntryData; import org.apache.fineract.accounting.journalentry.data.JournalEntryData;
import org.apache.fineract.accounting.journalentry.domain.JournalEntry; import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
@@ -277,14 +277,14 @@ public class ProductToGLAccountMappingHelper {
} }
final JsonArray chargeToIncomeAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayFragmentName, element); final JsonArray chargeToIncomeAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayFragmentName, element);
/** /*
* Variable stores a map representation of charges (key) and their associated income Id's (value) extracted from * Variable stores a map representation of charges (key) and their associated income Id's (value) extracted from
* the passed in Jsoncommand * the passed in Jsoncommand
**/ */
final Map<Long, Long> inputChargeToIncomeAccountMap = new HashMap<>(); final Map<Long, Long> inputChargeToIncomeAccountMap = new HashMap<>();
/*** /*
* Variable stores all charges which have already been mapped to Income Accounts in the system * Variable stores all charges which have already been mapped to Income Accounts in the system
**/ */
final Set<Long> existingCharges = new HashSet<>(); final Set<Long> existingCharges = new HashSet<>();
if (chargeToIncomeAccountMappingArray != null) { if (chargeToIncomeAccountMappingArray != null) {
if (changes != null) { if (changes != null) {
@@ -301,13 +301,13 @@ public class ProductToGLAccountMappingHelper {
// If input map is empty, delete all existing mappings // If input map is empty, delete all existing mappings
if (inputChargeToIncomeAccountMap.size() == 0) { if (inputChargeToIncomeAccountMap.size() == 0) {
this.accountMappingRepository.deleteAll(existingChargeToIncomeAccountMappings); this.accountMappingRepository.deleteAll(existingChargeToIncomeAccountMappings);
} /** } /*
* Else, <br/> * Else, <br/>
* update existing mappings OR <br/> * update existing mappings OR <br/>
* delete old mappings (which are already present, but not passed in as a part of Jsoncommand)<br/> * delete old mappings (which are already present, but not passed in as a part of Jsoncommand)<br/>
* Create new mappings for charges that are passed in as a part of the Jsoncommand but not already present * Create new mappings for charges that are passed in as a part of the Jsoncommand but not already present
* *
**/ */
else { else {
for (final ProductToGLAccountMapping chargeToIncomeAccountMapping : existingChargeToIncomeAccountMappings) { for (final ProductToGLAccountMapping chargeToIncomeAccountMapping : existingChargeToIncomeAccountMappings) {
final Long currentCharge = chargeToIncomeAccountMapping.getCharge().getId(); final Long currentCharge = chargeToIncomeAccountMapping.getCharge().getId();
@@ -356,14 +356,14 @@ public class ProductToGLAccountMappingHelper {
.findAllPaymentTypeToFundSourceMappings(productId, portfolioProductType.getValue()); .findAllPaymentTypeToFundSourceMappings(productId, portfolioProductType.getValue());
final JsonArray paymentChannelMappingArray = this.fromApiJsonHelper final JsonArray paymentChannelMappingArray = this.fromApiJsonHelper
.extractJsonArrayNamed(LoanProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), element); .extractJsonArrayNamed(LoanProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), element);
/** /*
* Variable stores a map representation of Payment channels (key) and their fund sources (value) extracted from * Variable stores a map representation of Payment channels (key) and their fund sources (value) extracted from
* the passed in Jsoncommand * the passed in Jsoncommand
**/ */
final Map<Long, Long> inputPaymentChannelFundSourceMap = new HashMap<>(); final Map<Long, Long> inputPaymentChannelFundSourceMap = new HashMap<>();
/*** /*
* Variable stores all payment types which have already been mapped to Fund Sources in the system * Variable stores all payment types which have already been mapped to Fund Sources in the system
**/ */
final Set<Long> existingPaymentTypes = new HashSet<>(); final Set<Long> existingPaymentTypes = new HashSet<>();
if (paymentChannelMappingArray != null) { if (paymentChannelMappingArray != null) {
if (changes != null) { if (changes != null) {
@@ -381,14 +381,14 @@ public class ProductToGLAccountMappingHelper {
// If input map is empty, delete all existing mappings // If input map is empty, delete all existing mappings
if (inputPaymentChannelFundSourceMap.isEmpty()) { if (inputPaymentChannelFundSourceMap.isEmpty()) {
this.accountMappingRepository.deleteAll(existingPaymentChannelToFundSourceMappings); this.accountMappingRepository.deleteAll(existingPaymentChannelToFundSourceMappings);
} /** } /*
* Else, <br/> * Else, <br/>
* update existing mappings OR <br/> * update existing mappings OR <br/>
* delete old mappings (which re already present, but not passed in as a part of Jsoncommand)<br/> * delete old mappings (which re already present, but not passed in as a part of Jsoncommand)<br/>
* Create new mappings for payment types that are passed in as a part of the Jsoncommand but not already * Create new mappings for payment types that are passed in as a part of the Jsoncommand but not already
* present * present
* *
**/ */
else { else {
for (final ProductToGLAccountMapping existingPaymentChannelToFundSourceMapping : existingPaymentChannelToFundSourceMappings) { for (final ProductToGLAccountMapping existingPaymentChannelToFundSourceMapping : existingPaymentChannelToFundSourceMappings) {
final Long currentPaymentChannelId = existingPaymentChannelToFundSourceMapping.getPaymentType().getId(); final Long currentPaymentChannelId = existingPaymentChannelToFundSourceMapping.getPaymentType().getId();
@@ -584,9 +584,9 @@ public class ProductToGLAccountMappingHelper {
// based on input condition // based on input condition
GLAccount glAccount; GLAccount glAccount;
/** /*
* Both CASH and Accrual placeholders have the same value for income from Interest and penalties * Both CASH and Accrual placeholders have the same value for income from Interest and penalties
**/ */
CashAccountsForLoan placeHolderAccountType; CashAccountsForLoan placeHolderAccountType;
if (isPenalty) { if (isPenalty) {
glAccount = getAccountByIdAndType(LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue(), GLAccountType.INCOME, glAccount = getAccountByIdAndType(LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue(), GLAccountType.INCOME,
@@ -181,12 +181,12 @@ public class TellerApiResource {
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Create Cashiers", description = """ @Operation(summary = "Create Cashiers", description = """
Mandatory Fields:\s Mandatory Fields:
Cashier/staff, Fromm Date, To Date, Full Day or From time and To time Cashier/staff, Fromm Date, To Date, Full Day or From time and To time
Optional Fields:\s Optional Fields:
Description/Notes""") Description/Notes""")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.PostTellersTellerIdCashiersRequest.class))) @RequestBody(required = true, content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.PostTellersTellerIdCashiersRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.PostTellersTellerIdCashiersResponse.class))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.PostTellersTellerIdCashiersResponse.class)))
@@ -76,6 +76,9 @@ public class Teller extends AbstractPersistableCustom<Long> {
@Column(name = "state", nullable = false) @Column(name = "state", nullable = false)
private Integer status; private Integer status;
@Column(name = "is_deleted", nullable = false)
private boolean deleted = false;
@OneToMany(mappedBy = "teller", fetch = FetchType.LAZY) @OneToMany(mappedBy = "teller", fetch = FetchType.LAZY)
private Set<Cashier> cashiers; private Set<Cashier> cashiers;
@@ -161,4 +164,12 @@ public class Teller extends AbstractPersistableCustom<Long> {
this.office.getId(); this.office.getId();
this.cashiers.size(); this.cashiers.size();
} }
public void delete() {
this.deleted = true;
}
public boolean isDeleted() {
return this.deleted;
}
} }
@@ -31,4 +31,6 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface TellerRepository extends JpaRepository<Teller, Long>, JpaSpecificationExecutor<Teller> { public interface TellerRepository extends JpaRepository<Teller, Long>, JpaSpecificationExecutor<Teller> {
Collection<Teller> findTellerByOfficeId(Long officeId); Collection<Teller> findTellerByOfficeId(Long officeId);
long countByDeletedFalse();
} }
@@ -36,6 +36,9 @@ public class TellerRepositoryWrapper {
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Teller findOneWithNotFoundDetection(final Long id) { public Teller findOneWithNotFoundDetection(final Long id) {
final Teller teller = this.repository.findById(id).orElseThrow(() -> new TellerNotFoundException(id)); final Teller teller = this.repository.findById(id).orElseThrow(() -> new TellerNotFoundException(id));
if (teller.isDeleted()) {
throw new TellerNotFoundException(id);
}
teller.initializeLazyCollections(); teller.initializeLazyCollections();
return teller; return teller;
} }
@@ -51,4 +54,8 @@ public class TellerRepositoryWrapper {
public void delete(final Teller teller) { public void delete(final Teller teller) {
this.repository.delete(teller); this.repository.delete(teller);
} }
public boolean existsAny() {
return this.repository.countByDeletedFalse() > 0;
}
} }
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.organisation.teller.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class TellerAlreadyExistsException extends AbstractPlatformDomainRuleException {
private static final String ERROR_MESSAGE_CODE = "error.msg.teller.already.exists";
private static final String DEFAULT_ERROR_MESSAGE = "Cannot create teller as a teller is already present.";
public TellerAlreadyExistsException() {
super(ERROR_MESSAGE_CODE, DEFAULT_ERROR_MESSAGE);
}
}
@@ -34,7 +34,8 @@ public enum CommandProcessingResultType {
AWAITING_APPROVAL(2, "commandProcessingResultType.awaiting.approval"), // AWAITING_APPROVAL(2, "commandProcessingResultType.awaiting.approval"), //
REJECTED(3, "commandProcessingResultType.rejected"), // REJECTED(3, "commandProcessingResultType.rejected"), //
UNDER_PROCESSING(4, "commandProcessingResultType.underProcessing"), // UNDER_PROCESSING(4, "commandProcessingResultType.underProcessing"), //
ERROR(5, "commandProcessingResultType.error"); ERROR(5, "commandProcessingResultType.error"), //
TERMINATE(6, "commandProcessingResultType.terminate");
private static final Map<Integer, CommandProcessingResultType> BY_ID = Arrays.stream(values()) private static final Map<Integer, CommandProcessingResultType> BY_ID = Arrays.stream(values())
.collect(Collectors.toMap(CommandProcessingResultType::getValue, v -> v)); .collect(Collectors.toMap(CommandProcessingResultType::getValue, v -> v));
@@ -59,4 +60,8 @@ public enum CommandProcessingResultType {
public boolean isRejected() { public boolean isRejected() {
return this == REJECTED; return this == REJECTED;
} }
public boolean isTerminate() {
return this == TERMINATE;
}
} }
@@ -71,6 +71,344 @@ public class CommandWrapperBuilder {
this.sanitizeJsonKeys); this.sanitizeJsonKeys);
} }
public CommandWrapperBuilder createChequeSeries() {
this.actionName = "CREATE";
this.entityName = "CHEQUESERIES";
this.entityId = null;
this.href = "/chequeseries";
return this;
}
public CommandWrapperBuilder updateChequeSeries(final Long seriesId) {
this.actionName = "UPDATE";
this.entityName = "CHEQUESERIES";
this.entityId = seriesId;
this.href = "/chequeseries/" + seriesId;
return this;
}
public CommandWrapperBuilder deleteChequeSeries(final Long seriesId) {
this.actionName = "DELETE";
this.entityName = "CHEQUESERIES";
this.entityId = seriesId;
this.href = "/chequeseries/" + seriesId;
return this;
}
public CommandWrapperBuilder createChequeStockLot() {
this.actionName = "CREATE";
this.entityName = "CHEQUESTOCKLOT";
this.entityId = null;
this.href = "/chequestocklots";
return this;
}
public CommandWrapperBuilder reportLostChequeStockLot(final Long lotId) {
this.actionName = "REPORTLOST";
this.entityName = "CHEQUESTOCKLOT";
this.entityId = lotId;
this.href = "/chequestocklots/" + lotId;
return this;
}
public CommandWrapperBuilder createChequeStockTransfer() {
this.actionName = "CREATE";
this.entityName = "CHEQUESTOCKTRANSFER";
this.entityId = null;
this.href = "/chequestocktransfers";
return this;
}
public CommandWrapperBuilder receiveChequeStockTransfer(final Long transferId) {
this.actionName = "RECEIVE";
this.entityName = "CHEQUESTOCKTRANSFER";
this.entityId = transferId;
this.href = "/chequestocktransfers/" + transferId;
return this;
}
public CommandWrapperBuilder rejectChequeStockTransfer(final Long transferId) {
this.actionName = "REJECT";
this.entityName = "CHEQUESTOCKTRANSFER";
this.entityId = transferId;
this.href = "/chequestocktransfers/" + transferId;
return this;
}
public CommandWrapperBuilder createChequeBook() {
this.actionName = "CREATE";
this.entityName = "CHEQUEBOOK";
this.entityId = null;
this.href = "/chequebooks";
return this;
}
public CommandWrapperBuilder issueChequeBook(final Long chequeBookId) {
this.actionName = "ISSUE";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder rejectChequeBook(final Long chequeBookId) {
this.actionName = "REJECT";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder withdrawChequeBook(final Long chequeBookId) {
this.actionName = "WITHDRAW";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder stopPaymentChequeBook(final Long chequeBookId) {
this.actionName = "STOPPAYMENT";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder revokeStopPaymentChequeBook(final Long chequeBookId) {
this.actionName = "REVOKESTOPPAYMENT";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder reportLostChequeBook(final Long chequeBookId) {
this.actionName = "REPORTLOST";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder cancelLeafChequeBook(final Long chequeBookId) {
this.actionName = "CANCELLEAF";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder markUsedChequeBook(final Long chequeBookId) {
this.actionName = "MARKUSED";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder returnChequeBook(final Long chequeBookId) {
this.actionName = "RETURN";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder approveChequeBookPendingRequest(final Long pendingRequestId) {
this.actionName = "APPROVE";
this.entityName = "CHEQUEBOOKPENDINGREQUEST";
this.entityId = pendingRequestId;
this.href = "/chequebook/pending-requests/" + pendingRequestId;
return this;
}
public CommandWrapperBuilder rejectChequeBookPendingRequest(final Long pendingRequestId) {
this.actionName = "REJECT";
this.entityName = "CHEQUEBOOKPENDINGREQUEST";
this.entityId = pendingRequestId;
this.href = "/chequebook/pending-requests/" + pendingRequestId;
return this;
}
// ===== Clearing (CTS + NACH) command wrappers =====
public CommandWrapperBuilder createClearingSettlementBatch() {
this.actionName = "CREATE";
this.entityName = "CLEARINGSETTLEMENTBATCH";
this.entityId = null;
this.href = "/clearing/settlement-batches";
return this;
}
public CommandWrapperBuilder postClearingSettlementBatch(final Long settlementBatchId) {
this.actionName = "POST";
this.entityName = "CLEARINGSETTLEMENTBATCH";
this.entityId = settlementBatchId;
this.href = "/clearing/settlement-batches/" + settlementBatchId;
return this;
}
public CommandWrapperBuilder rejectClearingSettlementBatch(final Long settlementBatchId) {
this.actionName = "REJECT";
this.entityName = "CLEARINGSETTLEMENTBATCH";
this.entityId = settlementBatchId;
this.href = "/clearing/settlement-batches/" + settlementBatchId;
return this;
}
public CommandWrapperBuilder createClearingReconciliation() {
this.actionName = "CREATE";
this.entityName = "CLEARINGRECONCILIATION";
this.entityId = null;
this.href = "/clearing/reconciliations";
return this;
}
public CommandWrapperBuilder createInwardClearingBatch() {
this.actionName = "CREATE";
this.entityName = "INWARDCLEARINGBATCH";
this.entityId = null;
this.href = "/ctsclearing/inward-batches";
return this;
}
public CommandWrapperBuilder validateInwardClearingBatch(final Long batchId) {
this.actionName = "VALIDATE";
this.entityName = "INWARDCLEARINGBATCH";
this.entityId = batchId;
this.href = "/ctsclearing/inward-batches/" + batchId;
return this;
}
public CommandWrapperBuilder passInwardClearingItem(final Long itemId) {
this.actionName = "PASS";
this.entityName = "INWARDCLEARINGITEM";
this.entityId = itemId;
this.href = "/ctsclearing/inward-items/" + itemId;
return this;
}
public CommandWrapperBuilder returnInwardClearingItem(final Long itemId) {
this.actionName = "RETURN";
this.entityName = "INWARDCLEARINGITEM";
this.entityId = itemId;
this.href = "/ctsclearing/inward-items/" + itemId;
return this;
}
public CommandWrapperBuilder verifyInwardClearingItemSignature(final Long itemId) {
this.actionName = "VERIFY_SIGNATURE";
this.entityName = "INWARDCLEARINGITEM";
this.entityId = itemId;
this.href = "/ctsclearing/inward-items/" + itemId;
return this;
}
public CommandWrapperBuilder createPositivePayRegistration() {
this.actionName = "CREATE";
this.entityName = "POSITIVEPAYREGISTRATION";
this.entityId = null;
this.href = "/ctsclearing/positive-pay";
return this;
}
public CommandWrapperBuilder cancelPositivePayRegistration(final Long registrationId) {
this.actionName = "CANCEL";
this.entityName = "POSITIVEPAYREGISTRATION";
this.entityId = registrationId;
this.href = "/ctsclearing/positive-pay/" + registrationId;
return this;
}
public CommandWrapperBuilder sendPendingOutwardClearingBatch() {
this.actionName = "SENDBATCH";
this.entityName = "OUTWARDCLEARINGPRESENTMENT";
this.entityId = null;
this.href = "/ctsclearing/outward-presentments/send-pending-batch";
return this;
}
public CommandWrapperBuilder createOutwardClearingPresentment() {
this.actionName = "CREATE";
this.entityName = "OUTWARDCLEARINGPRESENTMENT";
this.entityId = null;
this.href = "/ctsclearing/outward-presentments";
return this;
}
public CommandWrapperBuilder sendOutwardClearingPresentment(final Long presentmentId) {
this.actionName = "SEND";
this.entityName = "OUTWARDCLEARINGPRESENTMENT";
this.entityId = presentmentId;
this.href = "/ctsclearing/outward-presentments/" + presentmentId;
return this;
}
public CommandWrapperBuilder settleOutwardClearingPresentment(final Long presentmentId) {
this.actionName = "SETTLE";
this.entityName = "OUTWARDCLEARINGPRESENTMENT";
this.entityId = presentmentId;
this.href = "/ctsclearing/outward-presentments/" + presentmentId;
return this;
}
public CommandWrapperBuilder dishonourOutwardClearingPresentment(final Long presentmentId) {
this.actionName = "DISHONOUR";
this.entityName = "OUTWARDCLEARINGPRESENTMENT";
this.entityId = presentmentId;
this.href = "/ctsclearing/outward-presentments/" + presentmentId;
return this;
}
public CommandWrapperBuilder createNachMandate() {
this.actionName = "CREATE";
this.entityName = "NACHMANDATE";
this.entityId = null;
this.href = "/nachclearing/mandates";
return this;
}
public CommandWrapperBuilder cancelNachMandate(final Long mandateId) {
this.actionName = "CANCEL";
this.entityName = "NACHMANDATE";
this.entityId = mandateId;
this.href = "/nachclearing/mandates/" + mandateId;
return this;
}
public CommandWrapperBuilder createNachBatch() {
this.actionName = "CREATE";
this.entityName = "NACHBATCH";
this.entityId = null;
this.href = "/nachclearing/batches";
return this;
}
public CommandWrapperBuilder processNachBatch(final Long batchId) {
this.actionName = "PROCESS";
this.entityName = "NACHBATCH";
this.entityId = batchId;
this.href = "/nachclearing/batches/" + batchId;
return this;
}
public CommandWrapperBuilder passNachTransaction(final Long transactionId) {
this.actionName = "PASS";
this.entityName = "NACHTRANSACTION";
this.entityId = transactionId;
this.href = "/nachclearing/transactions/" + transactionId;
return this;
}
public CommandWrapperBuilder returnNachTransaction(final Long transactionId) {
this.actionName = "RETURN";
this.entityName = "NACHTRANSACTION";
this.entityId = transactionId;
this.href = "/nachclearing/transactions/" + transactionId;
return this;
}
public CommandWrapperBuilder updateCreditBureau() { public CommandWrapperBuilder updateCreditBureau() {
this.actionName = "UPDATE"; this.actionName = "UPDATE";
this.entityName = "ORGANISATIONCREDITBUREAU"; this.entityName = "ORGANISATIONCREDITBUREAU";
@@ -302,6 +640,14 @@ public class CommandWrapperBuilder {
return this; return this;
} }
public CommandWrapperBuilder createUserAgreement() {
this.actionName = "CREATE";
this.entityName = "USERAGREEMENT";
this.entityId = null;
this.href = "/useragreements";
return this;
}
public CommandWrapperBuilder createOffice() { public CommandWrapperBuilder createOffice() {
this.actionName = "CREATE"; this.actionName = "CREATE";
this.entityName = "OFFICE"; this.entityName = "OFFICE";
@@ -1611,6 +1957,14 @@ public class CommandWrapperBuilder {
return this; return this;
} }
public CommandWrapperBuilder terminateAccount(final Long accountId) {
this.actionName = "TERMINATE";
this.entityName = "ACCOUNT";
this.entityId = accountId;
this.href = "/accounts/" + accountId + "/terminate";
return this;
}
public CommandWrapperBuilder approveGSIMAccountApplication(final Long accountId) { public CommandWrapperBuilder approveGSIMAccountApplication(final Long accountId) {
this.actionName = "APPROVE"; this.actionName = "APPROVE";
this.entityName = "GSIMACCOUNT"; this.entityName = "GSIMACCOUNT";
@@ -3565,7 +3919,7 @@ public class CommandWrapperBuilder {
this.actionName = "ACTIVATE"; this.actionName = "ACTIVATE";
this.entityName = "CURRENTACCOUNT"; this.entityName = "CURRENTACCOUNT";
this.savingsId = accountId; this.savingsId = accountId;
this.entityId = null; this.entityId = accountId;
this.href = "/currentaccounts/" + accountId + "?command=activate"; this.href = "/currentaccounts/" + accountId + "?command=activate";
return this; return this;
} }
@@ -20,6 +20,7 @@ package org.apache.fineract.commands.service;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.ERROR; import static org.apache.fineract.commands.domain.CommandProcessingResultType.ERROR;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.PROCESSED; import static org.apache.fineract.commands.domain.CommandProcessingResultType.PROCESSED;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.TERMINATE;
import static org.apache.http.HttpStatus.SC_OK; import static org.apache.http.HttpStatus.SC_OK;
import com.google.gson.Gson; import com.google.gson.Gson;
@@ -181,7 +182,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
currentSource.setResultStatusCode(SC_OK); currentSource.setResultStatusCode(SC_OK);
currentSource.updateForAudit(result); currentSource.updateForAudit(result);
currentSource.setResult(toApiResultJsonSerializer.serializeResult(result)); currentSource.setResult(toApiResultJsonSerializer.serializeResult(result));
currentSource.setStatus(PROCESSED); currentSource.setStatus("TERMINATE".equals(wrapper.getActionName()) ? TERMINATE : PROCESSED);
// Return saved command source // Return saved command source
return commandSourceService.saveResultSameTransaction(currentSource); return commandSourceService.saveResultSameTransaction(currentSource);
@@ -21,6 +21,12 @@ package org.apache.fineract.infrastructure.core.serialization;
import com.google.gson.ExclusionStrategy; import com.google.gson.ExclusionStrategy;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalTime; import java.time.LocalTime;
@@ -112,5 +118,33 @@ public final class GoogleGsonSerializerHelper {
builder.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter()); builder.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter());
builder.registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter()); builder.registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter());
builder.registerTypeAdapter(ExternalId.class, new ExternalIdAdapter()); builder.registerTypeAdapter(ExternalId.class, new ExternalIdAdapter());
// Use runtime type when serializing interface-typed fields (e.g. Collection<? extends IGuarantor>)
builder.registerTypeAdapterFactory(new TypeAdapterFactory() {
@Override
@SuppressWarnings("unchecked")
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type) {
if (!type.getRawType().isInterface()) {
return null;
}
final TypeAdapterFactory self = this;
return new TypeAdapter<T>() {
@Override
public void write(final JsonWriter out, final T value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
TypeAdapter<T> delegate = (TypeAdapter<T>) gson.getDelegateAdapter(self,
TypeToken.get((Class<T>) value.getClass()));
delegate.write(out, value);
}
@Override
public T read(final JsonReader in) throws IOException {
return null;
}
};
}
});
} }
} }
@@ -0,0 +1,56 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.documentmanagement.adapter;
import java.io.Serial;
import java.io.Serializable;
import java.util.Optional;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Decouples the entity "current signature" pointer from the generic document service, the same
* way {@link EntityImageIdAdapter} decouples the "current photo" pointer — lets
* DocumentWritePlatformServiceImpl (fineract-document module) give single-slot upsert semantics to
* signature uploads without depending on portfolio-module entities directly.
*/
public interface EntitySignatureIdAdapter {
/** @param documentName the m_document.name of the document being created/removed */
boolean accept(String entityType, String documentName);
Optional<SignatureIdResult> get(Long entityId);
Optional<SignatureIdResult> set(Long entityId, Long signatureId);
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
class SignatureIdResult implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long id;
private String displayName;
}
}
@@ -61,6 +61,12 @@ public enum JobName {
ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS("Add Accrual Transactions For Savings"), // ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS("Add Accrual Transactions For Savings"), //
JOURNAL_ENTRY_AGGREGATION("Journal Entry Aggregation"), // JOURNAL_ENTRY_AGGREGATION("Journal Entry Aggregation"), //
FETCH_RBI_RSS_FEEDS("Fetch RBI RSS Feeds"), // FETCH_RBI_RSS_FEEDS("Fetch RBI RSS Feeds"), //
IMPS_DEEMED_RECONCILE("IMPS DEEMED Reconciliation"), //
NEFT_BATCH_PROCESS("NEFT Batch Process"), //
VERIFICATION_TOKEN_CLEANUP("Verification Token Cleanup"), //
OUTWARD_CLEARING_BATCH_SEND("CTS Outward Clearing Batch Send"), //
CTR_REPORT_GENERATION("AML Monthly CTR Report Generation"), //
AML_STR_WEEKLY_DIGEST("AML STR Weekly Digest"), //
; // ; //
private final String name; private final String name;
@@ -23,6 +23,7 @@ public enum PortfolioAccountType {
INVALID(0, "accountType.invalid"), // INVALID(0, "accountType.invalid"), //
LOAN(1, "accountType.loan"), // LOAN(1, "accountType.loan"), //
SAVINGS(2, "accountType.savings"), // SAVINGS(2, "accountType.savings"), //
CURRENT(3, "accountType.current"), //
; ;
private final Integer value; private final Integer value;
@@ -52,6 +53,9 @@ public enum PortfolioAccountType {
case 2: case 2:
enumType = SAVINGS; enumType = SAVINGS;
break; break;
case 3:
enumType = CURRENT;
break;
default: default:
enumType = INVALID; enumType = INVALID;
} }
@@ -68,4 +72,8 @@ public enum PortfolioAccountType {
public boolean isLoanAccount() { public boolean isLoanAccount() {
return this.equals(LOAN); return this.equals(LOAN);
} }
public boolean isCurrentAccount() {
return this.equals(CURRENT);
}
} }
@@ -0,0 +1,160 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.useradministration.domain.AppUser;
/**
* One row per {@link AmlTransactionRule} hit on a live transaction. Raised regardless of the
* client's current KYC risk category - alerting is never suppressed by a client being Low/Medium
* risk. First reviewed by an AML Officer (whitelist or escalate); an ESCALATED alert spawns an
* {@link AmlStrCase} for the Principal Officer.
*/
@Entity
@Table(name = "m_aml_alert")
public class AmlAlert extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "transaction_type", nullable = false, length = 20)
private String transactionType;
@Column(name = "transaction_id", nullable = false)
private Long transactionId;
@ManyToOne
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@ManyToOne
@JoinColumn(name = "rule_id", nullable = false)
private AmlTransactionRule rule;
@Column(name = "amount", nullable = false, precision = 19, scale = 6)
private BigDecimal amount;
@Column(name = "currency_code", length = 3)
private String currencyCode;
@Column(name = "raised_on", nullable = false)
private LocalDateTime raisedOn;
@Column(name = "status", nullable = false, length = 20)
private String status;
@ManyToOne
@JoinColumn(name = "reviewed_by")
private AppUser reviewedBy;
@Column(name = "reviewed_on")
private LocalDateTime reviewedOn;
@Column(name = "review_notes", length = 1000)
private String reviewNotes;
@Column(name = "disposition", length = 20)
private String disposition;
protected AmlAlert() {}
public static AmlAlert raise(AmlTransactionType transactionType, Long transactionId, Client client, AmlTransactionRule rule,
BigDecimal amount, String currencyCode, LocalDateTime raisedOn) {
final AmlAlert alert = new AmlAlert();
alert.transactionType = transactionType.name();
alert.transactionId = transactionId;
alert.client = client;
alert.rule = rule;
alert.amount = amount;
alert.currencyCode = currencyCode;
alert.raisedOn = raisedOn;
alert.status = AmlAlertStatus.RAISED.name();
return alert;
}
public void whitelist(AppUser reviewer, String notes) {
this.reviewedBy = reviewer;
this.reviewedOn = LocalDateTime.now();
this.reviewNotes = notes;
this.disposition = AmlAlertDisposition.WHITELIST.name();
this.status = AmlAlertStatus.WHITELISTED.name();
}
public void escalate(AppUser reviewer, String notes) {
this.reviewedBy = reviewer;
this.reviewedOn = LocalDateTime.now();
this.reviewNotes = notes;
this.disposition = AmlAlertDisposition.ESCALATE.name();
this.status = AmlAlertStatus.ESCALATED.name();
}
public Long getTransactionId() {
return transactionId;
}
public AmlTransactionType getTransactionType() {
return AmlTransactionType.valueOf(transactionType);
}
public Client getClient() {
return client;
}
public AmlTransactionRule getRule() {
return rule;
}
public BigDecimal getAmount() {
return amount;
}
public String getCurrencyCode() {
return currencyCode;
}
public LocalDateTime getRaisedOn() {
return raisedOn;
}
public AmlAlertStatus getStatus() {
return AmlAlertStatus.valueOf(status);
}
public AppUser getReviewedBy() {
return reviewedBy;
}
public LocalDateTime getReviewedOn() {
return reviewedOn;
}
public String getReviewNotes() {
return reviewNotes;
}
public AmlAlertDisposition getDisposition() {
return disposition == null ? null : AmlAlertDisposition.valueOf(disposition);
}
}
@@ -0,0 +1,25 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
/** The AML Officer's first-level disposition of an {@link AmlAlert}. */
public enum AmlAlertDisposition {
WHITELIST, ESCALATE
}
@@ -0,0 +1,44 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface AmlAlertRepository extends JpaRepository<AmlAlert, Long> {
List<AmlAlert> findByStatusOrderByRaisedOnAsc(String status);
@Query("SELECT COALESCE(SUM(a.amount), 0) FROM AmlAlert a WHERE a.client.id = :clientId "
+ "AND a.transactionType = :transactionType AND a.raisedOn >= :since")
BigDecimal sumAmountForClientSince(@Param("clientId") Long clientId, @Param("transactionType") String transactionType,
@Param("since") LocalDateTime since);
long countByClientId(Long clientId);
/**
* Alerts already raised against one transaction under one rule. Used to stop the same ledger entry being marked
* twice, which would put two STR cases in front of the Principal Officer for a single transaction.
*/
List<AmlAlert> findByRule_IdAndTransactionId(Long ruleId, Long transactionId);
}
@@ -0,0 +1,24 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
public enum AmlAlertStatus {
RAISED, UNDER_REVIEW, WHITELISTED, ESCALATED
}
@@ -0,0 +1,29 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
/** Which transaction type(s) an {@link AmlTransactionRule} evaluates against. */
public enum AmlAppliesTo {
SAVINGS, LOAN, TRANSFER, ALL;
public boolean matches(AmlTransactionType transactionType) {
return this == ALL || this.name().equals(transactionType.name());
}
}
@@ -0,0 +1,97 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/**
* Monthly, mechanical CTR batch header - purely threshold-based (₹10L+ cash, individually or
* connected), no Principal Officer judgment involved. One row per calendar month.
*/
@Entity
@Table(name = "m_aml_ctr_report")
public class AmlCtrReport extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "report_month", nullable = false, length = 7)
private String reportMonth;
@Column(name = "generated_on", nullable = false)
private LocalDateTime generatedOn;
@Column(name = "record_count", nullable = false)
private int recordCount;
@Column(name = "file_reference", length = 150)
private String fileReference;
@Column(name = "status", nullable = false, length = 20)
private String status;
@Column(name = "submitted_on")
private LocalDateTime submittedOn;
protected AmlCtrReport() {}
public static AmlCtrReport draft(String reportMonth, LocalDateTime generatedOn, int recordCount) {
final AmlCtrReport report = new AmlCtrReport();
report.reportMonth = reportMonth;
report.generatedOn = generatedOn;
report.recordCount = recordCount;
report.status = AmlCtrReportStatus.DRAFT.name();
return report;
}
public void markGenerated(String fileReference) {
this.fileReference = fileReference;
this.status = AmlCtrReportStatus.GENERATED.name();
}
public void markSubmitted(LocalDateTime submittedOn) {
this.submittedOn = submittedOn;
this.status = AmlCtrReportStatus.SUBMITTED.name();
}
public String getReportMonth() {
return reportMonth;
}
public LocalDateTime getGeneratedOn() {
return generatedOn;
}
public int getRecordCount() {
return recordCount;
}
public String getFileReference() {
return fileReference;
}
public AmlCtrReportStatus getStatus() {
return AmlCtrReportStatus.valueOf(status);
}
public LocalDateTime getSubmittedOn() {
return submittedOn;
}
}
@@ -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.aml.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.math.BigDecimal;
/**
* One line per transaction (or connected-transaction group, when {@link #connectedGroup} is true)
* feeding a monthly {@link AmlCtrReport}.
*/
@Entity
@Table(name = "m_aml_ctr_report_line")
public class AmlCtrReportLine {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "ctr_report_id", nullable = false)
private AmlCtrReport ctrReport;
@Column(name = "transaction_type", nullable = false, length = 20)
private String transactionType;
@Column(name = "transaction_id")
private Long transactionId;
@Column(name = "client_id", nullable = false)
private Long clientId;
@Column(name = "amount", nullable = false, precision = 19, scale = 6)
private BigDecimal amount;
@Column(name = "is_connected_group", nullable = false)
private boolean connectedGroup;
protected AmlCtrReportLine() {}
public static AmlCtrReportLine of(AmlCtrReport ctrReport, AmlTransactionType transactionType, Long transactionId, Long clientId,
BigDecimal amount, boolean connectedGroup) {
final AmlCtrReportLine line = new AmlCtrReportLine();
line.ctrReport = ctrReport;
line.transactionType = transactionType.name();
line.transactionId = transactionId;
line.clientId = clientId;
line.amount = amount;
line.connectedGroup = connectedGroup;
return line;
}
public Long getId() {
return id;
}
public AmlCtrReport getCtrReport() {
return ctrReport;
}
public AmlTransactionType getTransactionType() {
return AmlTransactionType.valueOf(transactionType);
}
public Long getTransactionId() {
return transactionId;
}
public Long getClientId() {
return clientId;
}
public BigDecimal getAmount() {
return amount;
}
public boolean isConnectedGroup() {
return connectedGroup;
}
}
@@ -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.aml.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AmlCtrReportLineRepository extends JpaRepository<AmlCtrReportLine, Long> {
List<AmlCtrReportLine> findByCtrReport_Id(Long ctrReportId);
}
@@ -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.aml.domain;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AmlCtrReportRepository extends JpaRepository<AmlCtrReport, Long> {
Optional<AmlCtrReport> findByReportMonth(String reportMonth);
}
@@ -0,0 +1,24 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
public enum AmlCtrReportStatus {
DRAFT, GENERATED, SUBMITTED
}
@@ -0,0 +1,33 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
/**
* How an {@link AmlTransactionRule} is evaluated.
*
* <ul>
* <li>THRESHOLD - a single transaction at or above {@code thresholdAmount}</li>
* <li>STRUCTURING - connected transactions summing at/above {@code thresholdAmount} within {@code windowDays}</li>
* <li>VELOCITY - unusual frequency of transactions within {@code windowDays}, independent of a fixed amount</li>
* </ul>
*/
public enum AmlRuleType {
THRESHOLD, STRUCTURING, VELOCITY
}
@@ -0,0 +1,164 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDate;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.useradministration.domain.AppUser;
/**
* Created when an {@link AmlAlert} is escalated past first-level review. {@link #concludedOn} is
* the Principal Officer's conclusion date - the RBI-mandated 7-working-day FIU filing clock
* ({@link #fiuDueDate}) starts here, never at the alert or transaction date.
*/
@Entity
@Table(name = "m_aml_str_case")
public class AmlStrCase extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne
@JoinColumn(name = "alert_id", nullable = false)
private AmlAlert alert;
@ManyToOne
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@ManyToOne
@JoinColumn(name = "assigned_po")
private AppUser assignedPo;
@Column(name = "investigation_notes", length = 2000)
private String investigationNotes;
@Column(name = "source_of_funds_notes", length = 2000)
private String sourceOfFundsNotes;
@Column(name = "conclusion", length = 20)
private String conclusion;
@ManyToOne
@JoinColumn(name = "concluded_by")
private AppUser concludedBy;
@Column(name = "concluded_on")
private LocalDate concludedOn;
@Column(name = "fiu_due_date")
private LocalDate fiuDueDate;
@Column(name = "str_filed_on")
private LocalDate strFiledOn;
@Column(name = "str_reference_no", length = 100)
private String strReferenceNo;
@Column(name = "status", nullable = false, length = 25)
private String status;
protected AmlStrCase() {}
public static AmlStrCase openFor(AmlAlert alert) {
final AmlStrCase strCase = new AmlStrCase();
strCase.alert = alert;
strCase.client = alert.getClient();
strCase.status = AmlStrCaseStatus.OPEN.name();
return strCase;
}
public void assignPrincipalOfficer(AppUser po) {
this.assignedPo = po;
}
/** {@code fiuDueDate} is computed by the caller using the working-days calendar, not here. */
public void conclude(AppUser po, AmlStrConclusion conclusion, String investigationNotes, String sourceOfFundsNotes,
LocalDate concludedOn, LocalDate fiuDueDate) {
this.concludedBy = po;
this.conclusion = conclusion.name();
this.investigationNotes = investigationNotes;
this.sourceOfFundsNotes = sourceOfFundsNotes;
this.concludedOn = concludedOn;
this.status = conclusion == AmlStrConclusion.SUSPICIOUS ? AmlStrCaseStatus.CONCLUDED_SUSPICIOUS.name()
: AmlStrCaseStatus.CONCLUDED_CLEAR.name();
this.fiuDueDate = conclusion == AmlStrConclusion.SUSPICIOUS ? fiuDueDate : null;
}
public void fileStr(String referenceNo, LocalDate filedOn) {
this.strReferenceNo = referenceNo;
this.strFiledOn = filedOn;
this.status = AmlStrCaseStatus.FILED.name();
}
public Long getClientId() {
return client.getId();
}
public AmlAlert getAlert() {
return alert;
}
public Client getClient() {
return client;
}
public AppUser getAssignedPo() {
return assignedPo;
}
public String getInvestigationNotes() {
return investigationNotes;
}
public String getSourceOfFundsNotes() {
return sourceOfFundsNotes;
}
public AmlStrConclusion getConclusion() {
return conclusion == null ? null : AmlStrConclusion.valueOf(conclusion);
}
public AppUser getConcludedBy() {
return concludedBy;
}
public LocalDate getConcludedOn() {
return concludedOn;
}
public LocalDate getFiuDueDate() {
return fiuDueDate;
}
public LocalDate getStrFiledOn() {
return strFiledOn;
}
public String getStrReferenceNo() {
return strReferenceNo;
}
public AmlStrCaseStatus getStatus() {
return AmlStrCaseStatus.valueOf(status);
}
}
@@ -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.aml.domain;
import java.time.LocalDate;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface AmlStrCaseRepository extends JpaRepository<AmlStrCase, Long> {
long countByClient_Id(Long clientId);
List<AmlStrCase> findByStatus(String status);
@Query("SELECT c FROM AmlStrCase c WHERE c.status = 'CONCLUDED_SUSPICIOUS' AND c.concludedOn BETWEEN :from AND :to")
List<AmlStrCase> findConcludedSuspiciousBetween(@Param("from") LocalDate from, @Param("to") LocalDate to);
@Query("SELECT c FROM AmlStrCase c WHERE c.status = 'CONCLUDED_SUSPICIOUS' AND c.fiuDueDate < :asOf")
List<AmlStrCase> findOverdueForFiling(@Param("asOf") LocalDate asOf);
}
@@ -0,0 +1,24 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
public enum AmlStrCaseStatus {
OPEN, CONCLUDED_CLEAR, CONCLUDED_SUSPICIOUS, FILED
}
@@ -0,0 +1,25 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
/** The Principal Officer's final conclusion on an {@link AmlStrCase}. */
public enum AmlStrConclusion {
NOT_SUSPICIOUS, SUSPICIOUS
}
@@ -0,0 +1,140 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
/**
* A machine-evaluable transaction-monitoring rule. Distinct from {@code ClientRiskRule}
* (client-profile, KYC-time scoring) - these are checked against live savings/loan/transfer
* transactions as they post. {@link #sourceClientRiskRuleId} optionally traces a rule back to its
* originating entry in the 221-rule AML catalog seeded in {@code m_client_risk_rule}.
*/
@Entity
@Table(name = "m_aml_transaction_rule")
public class AmlTransactionRule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "rule_code", nullable = false, length = 50)
private String ruleCode;
@Column(name = "description", nullable = false, length = 500)
private String description;
@Column(name = "applies_to", nullable = false, length = 20)
private String appliesTo;
@Column(name = "threshold_amount", precision = 19, scale = 6)
private BigDecimal thresholdAmount;
/** Risk-category-tiered overrides of {@link #thresholdAmount}. Null on both means the rule is
* not tiered; {@link #thresholdAmount} is used for every client regardless of risk category. */
@Column(name = "threshold_amount_medium_risk", precision = 19, scale = 6)
private BigDecimal thresholdAmountMediumRisk;
@Column(name = "threshold_amount_high_risk", precision = 19, scale = 6)
private BigDecimal thresholdAmountHighRisk;
/**
* Declarative band the rule covers, shown on the AML Rules Master screen. Purely
* descriptive: unlike the tiered columns above these never take part in matching, so
* changing them cannot alter which transactions raise an alert. A null maximum means
* the rule is genuinely open-ended.
*/
@Column(name = "min_limit_amount", precision = 19, scale = 6)
private BigDecimal minLimitAmount;
@Column(name = "max_limit_amount", precision = 19, scale = 6)
private BigDecimal maxLimitAmount;
@Column(name = "rule_type", nullable = false, length = 20)
private String ruleType;
@Column(name = "window_days")
private Integer windowDays;
@Column(name = "source_client_risk_rule_id")
private Long sourceClientRiskRuleId;
@Column(name = "is_active", nullable = false)
private boolean active;
protected AmlTransactionRule() {}
public Long getId() {
return id;
}
public String getRuleCode() {
return ruleCode;
}
public String getDescription() {
return description;
}
public AmlAppliesTo getAppliesTo() {
return AmlAppliesTo.valueOf(appliesTo);
}
public BigDecimal getThresholdAmount() {
return thresholdAmount;
}
public BigDecimal getThresholdAmountMediumRisk() {
return thresholdAmountMediumRisk;
}
public BigDecimal getThresholdAmountHighRisk() {
return thresholdAmountHighRisk;
}
public BigDecimal getMinLimitAmount() {
return minLimitAmount;
}
public BigDecimal getMaxLimitAmount() {
return maxLimitAmount;
}
public AmlRuleType getRuleType() {
return AmlRuleType.valueOf(ruleType);
}
public Integer getWindowDays() {
return windowDays;
}
public Long getSourceClientRiskRuleId() {
return sourceClientRiskRuleId;
}
public boolean isActive() {
return active;
}
}
@@ -0,0 +1,35 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AmlTransactionRuleRepository extends JpaRepository<AmlTransactionRule, Long> {
List<AmlTransactionRule> findByActiveTrueAndAppliesTo(String appliesTo);
/**
* Looks a rule up by its code irrespective of {@code is_active}. Needed for MANUAL_REFERRAL, which is deliberately
* seeded inactive so the evaluation engine never fires it, yet must still be attachable to an alert a user raises
* by hand from the ledger.
*/
Optional<AmlTransactionRule> findByRuleCode(String ruleCode);
}
@@ -0,0 +1,25 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
/** The kind of ledger transaction an {@link AmlAlert} was raised against. */
public enum AmlTransactionType {
SAVINGS, LOAN, TRANSFER
}
@@ -0,0 +1,138 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.useradministration.domain.AppUser;
/**
* Immutable, append-only audit trail of a client's KYC risk-category changes. Backs the AML menu's
* existing "Risk Update History Report" item, which previously had no data behind it. Written both
* for manual re-categorization and for the automatic escalation triggered on a client's 3rd
* cumulative {@link AmlStrCase}.
*/
@Entity
@Table(name = "m_client_risk_category_history")
public class ClientRiskCategoryHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@Column(name = "previous_category", nullable = false)
private Integer previousCategory;
@Column(name = "new_category", nullable = false)
private Integer newCategory;
@Column(name = "reason", nullable = false, length = 500)
private String reason;
@Column(name = "triggered_by", nullable = false, length = 30)
private String triggeredBy;
@ManyToOne
@JoinColumn(name = "str_case_id")
private AmlStrCase strCase;
@Column(name = "changed_on", nullable = false)
private LocalDateTime changedOn;
@ManyToOne
@JoinColumn(name = "changed_by", nullable = false)
private AppUser changedBy;
protected ClientRiskCategoryHistory() {}
public static ClientRiskCategoryHistory autoEscalation(Client client, Integer previousCategory, Integer newCategory,
AmlStrCase triggeringCase, AppUser systemActor, LocalDateTime changedOn) {
final ClientRiskCategoryHistory history = new ClientRiskCategoryHistory();
history.client = client;
history.previousCategory = previousCategory;
history.newCategory = newCategory;
history.reason = "Auto-escalated after client's 3rd cumulative AML STR case (case #" + triggeringCase.getId() + ")";
history.triggeredBy = RiskCategoryChangeTrigger.AML_AUTO_ESCALATION.name();
history.strCase = triggeringCase;
history.changedOn = changedOn;
history.changedBy = systemActor;
return history;
}
public static ClientRiskCategoryHistory manual(Client client, Integer previousCategory, Integer newCategory, String reason,
AppUser changedBy, LocalDateTime changedOn) {
final ClientRiskCategoryHistory history = new ClientRiskCategoryHistory();
history.client = client;
history.previousCategory = previousCategory;
history.newCategory = newCategory;
history.reason = reason;
history.triggeredBy = RiskCategoryChangeTrigger.MANUAL.name();
history.changedBy = changedBy;
history.changedOn = changedOn;
return history;
}
public Long getId() {
return id;
}
public Client getClient() {
return client;
}
public Integer getPreviousCategory() {
return previousCategory;
}
public Integer getNewCategory() {
return newCategory;
}
public String getReason() {
return reason;
}
public RiskCategoryChangeTrigger getTriggeredBy() {
return RiskCategoryChangeTrigger.valueOf(triggeredBy);
}
public AmlStrCase getStrCase() {
return strCase;
}
public LocalDateTime getChangedOn() {
return changedOn;
}
public AppUser getChangedBy() {
return changedBy;
}
}
@@ -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.aml.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ClientRiskCategoryHistoryRepository extends JpaRepository<ClientRiskCategoryHistory, Long> {
List<ClientRiskCategoryHistory> findByClient_IdOrderByChangedOnDesc(Long clientId);
}
@@ -0,0 +1,24 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
public enum RiskCategoryChangeTrigger {
AML_AUTO_ESCALATION, MANUAL
}
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* Simulator-side audit log for CFCFRMS (I4C/MHA) events. * Simulator-side audit log for CFCFRMS (I4C/MHA) events.
@@ -69,7 +70,7 @@ public class CfcfrmsSimulatorLog extends AbstractAuditableWithUTCDateTimeCustom<
this.eventType = eventType; this.eventType = eventType;
this.payload = payload; this.payload = payload;
this.responsePayload = responsePayload; this.responsePayload = responsePayload;
this.eventTimestamp = OffsetDateTime.now(); this.eventTimestamp = DateUtils.getOffsetDateTimeOfTenant();
} }
public CfcfrmsSimulatorLog(final String cfcfrmsRef, final String ticketId, final String bankIfsc, public CfcfrmsSimulatorLog(final String cfcfrmsRef, final String ticketId, final String bankIfsc,
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* Audit trail for every status change on a CFCFRMS complaint ticket. * Audit trail for every status change on a CFCFRMS complaint ticket.
@@ -54,7 +55,7 @@ public class ComplaintAuditLog extends AbstractAuditableWithUTCDateTimeCustom<Lo
this.event = event; this.event = event;
this.description = description; this.description = description;
this.actor = actor; this.actor = actor;
this.eventTimestamp = OffsetDateTime.now(); this.eventTimestamp = DateUtils.getOffsetDateTimeOfTenant();
} }
public ComplaintAuditLog(final String ticketId, final String event, final String description, public ComplaintAuditLog(final String ticketId, final String event, final String description,
@@ -26,6 +26,7 @@ import jakarta.persistence.Table;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* CFCFRMS Complaint Ticket — represents a cyber fraud complaint pushed by MHA/I4C to the bank. * CFCFRMS Complaint Ticket — represents a cyber fraud complaint pushed by MHA/I4C to the bank.
@@ -121,7 +122,7 @@ public class ComplaintTicket extends AbstractAuditableWithUTCDateTimeCustom<Long
ticket.status = ComplaintStatus.RECEIVED; ticket.status = ComplaintStatus.RECEIVED;
ticket.assignedOfficer = assignedOfficer; ticket.assignedOfficer = assignedOfficer;
ticket.statePoliceRef = statePoliceRef; ticket.statePoliceRef = statePoliceRef;
ticket.receivedAt = OffsetDateTime.now(); ticket.receivedAt = DateUtils.getOffsetDateTimeOfTenant();
return ticket; return ticket;
} }
@@ -132,7 +133,7 @@ public class ComplaintTicket extends AbstractAuditableWithUTCDateTimeCustom<Long
this.status = newStatus; this.status = newStatus;
this.actionTaken = actionNote; this.actionTaken = actionNote;
if (newStatus == ComplaintStatus.RESOLVED || newStatus == ComplaintStatus.REJECTED) { if (newStatus == ComplaintStatus.RESOLVED || newStatus == ComplaintStatus.REJECTED) {
this.resolvedAt = OffsetDateTime.now(); this.resolvedAt = DateUtils.getOffsetDateTimeOfTenant();
} }
} }
@@ -0,0 +1,66 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.adapter;
import static java.util.Objects.nonNull;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.documentmanagement.adapter.EntitySignatureIdAdapter;
import org.apache.fineract.portfolio.client.domain.ClientRepository;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@RequiredArgsConstructor
@Component
class ClientSignatureIdAdapter implements EntitySignatureIdAdapter {
private static final String ENTITY_TYPE = "clients";
private static final String DOCUMENT_NAME = "clientSignature";
private final ClientRepository repository;
@Override
public boolean accept(String entityType, String documentName) {
return ENTITY_TYPE.equalsIgnoreCase(entityType) && DOCUMENT_NAME.equalsIgnoreCase(documentName);
}
@Override
@Transactional(readOnly = true)
public Optional<SignatureIdResult> get(Long entityId) {
return repository.findById(entityId).filter(client -> nonNull(client.getSignatureId()))
.map(client -> SignatureIdResult.builder().id(client.getSignatureId()).displayName(client.getDisplayName()).build());
}
@Override
@Transactional
public Optional<SignatureIdResult> set(Long entityId, Long signatureId) {
final var result = get(entityId);
if (signatureId == null) {
repository.removeSignatureId(entityId);
} else {
repository.updateByIdAndSignatureId(entityId, signatureId);
}
return result;
}
}
@@ -63,6 +63,7 @@ public class ClientApiConstants {
public static final String CLIENT_CLASSIFICATION = "ClientClassification"; public static final String CLIENT_CLASSIFICATION = "ClientClassification";
public static final String OCCUPATION_CATEGORY = "OccupationCategory"; public static final String OCCUPATION_CATEGORY = "OccupationCategory";
public static final String PWD_CATEGORY = "PwdCategory";
public static final String OCCUPATION = "Occupation"; public static final String OCCUPATION = "Occupation";
public static final String CLIENT_NON_PERSON_CONSTITUTION = "Constitution"; public static final String CLIENT_NON_PERSON_CONSTITUTION = "Constitution";
@@ -96,6 +97,15 @@ public class ClientApiConstants {
public static final String mobileNoParamName = "mobileNo"; public static final String mobileNoParamName = "mobileNo";
public static final String alternateMobileNoParamName = "alternateMobileNo"; public static final String alternateMobileNoParamName = "alternateMobileNo";
public static final String emailAddressParamName = "emailAddress"; public static final String emailAddressParamName = "emailAddress";
// Dual-OTP contact-change verification (see ClientContactOtpService) — required only when
// mobileNo or emailAddress is being changed via PUT /clients/{id}. Reference an already
// VERIFIED m_otp_log row for the OLD destination (current DB value) and the NEW destination
// (incoming value) respectively. Field-specific (not a single generic pair) so mobileNo and
// emailAddress can both change in the same request without ambiguity.
public static final String mobileOldOtpIdParamName = "mobileOldOtpId";
public static final String mobileNewOtpIdParamName = "mobileNewOtpId";
public static final String emailOldOtpIdParamName = "emailOldOtpId";
public static final String emailNewOtpIdParamName = "emailNewOtpId";
public static final String firstnameParamName = "firstname"; public static final String firstnameParamName = "firstname";
public static final String middlenameParamName = "middlename"; public static final String middlenameParamName = "middlename";
public static final String lastnameParamName = "lastname"; public static final String lastnameParamName = "lastname";
@@ -138,7 +148,13 @@ public class ClientApiConstants {
public static final String religionParamName = "religion"; public static final String religionParamName = "religion";
public static final String socialCategoryParamName = "socialCategory"; public static final String socialCategoryParamName = "socialCategory";
public static final String monthlyIncomeParamName = "monthlyIncome"; public static final String monthlyIncomeParamName = "monthlyIncome";
public static final String annualIncomeParamName = "annualIncome";
public static final String annualTurnoverParamName = "annualTurnover"; public static final String annualTurnoverParamName = "annualTurnover";
public static final String monthlyTurnoverParamName = "monthlyTurnover";
public static final String isSeniorCitizenParamName = "isSeniorCitizen";
public static final String isPwdParamName = "isPwd";
public static final String pwdCategoryIdParamName = "pwdCategoryId";
public static final String pwdCategoryParamName = "pwdCategory";
public static final String agricultureIncomeParamName = "agricultureIncome"; public static final String agricultureIncomeParamName = "agricultureIncome";
public static final String nonAgricultureIncomeParamName = "nonAgricultureIncome"; public static final String nonAgricultureIncomeParamName = "nonAgricultureIncome";
public static final String ageYearsParamName = "ageYears"; public static final String ageYearsParamName = "ageYears";
@@ -248,8 +264,9 @@ public class ClientApiConstants {
staffOptionsParamName, dateOfBirthParamName, genderParamName, clientTypeParamName, clientClassificationParamName, staffOptionsParamName, dateOfBirthParamName, genderParamName, clientTypeParamName, clientClassificationParamName,
salutationParamName, occupationCategoryParamName, occupationParamName, maritalStatusParamName, salutationParamName, occupationCategoryParamName, occupationParamName, maritalStatusParamName,
educationLevelParamName, degreeQualificationParamName, educationLevelParamName, degreeQualificationParamName,
religionParamName, socialCategoryParamName, monthlyIncomeParamName, religionParamName, socialCategoryParamName, monthlyIncomeParamName, annualIncomeParamName,
annualTurnoverParamName, agricultureIncomeParamName, nonAgricultureIncomeParamName, annualTurnoverParamName, monthlyTurnoverParamName, agricultureIncomeParamName, nonAgricultureIncomeParamName,
isSeniorCitizenParamName, isPwdParamName, pwdCategoryParamName,
ageYearsParamName, ageMonthsParamName, ageDaysParamName, ageYearsParamName, ageMonthsParamName, ageDaysParamName,
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianClientDisplayNameParamName, guardianRelationParamName, nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianClientDisplayNameParamName, guardianRelationParamName,
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName, dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
@@ -82,7 +82,13 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
private CodeValueData religion; private CodeValueData religion;
private CodeValueData socialCategory; private CodeValueData socialCategory;
private BigDecimal monthlyIncome; private BigDecimal monthlyIncome;
private BigDecimal annualIncome;
private BigDecimal annualTurnover; private BigDecimal annualTurnover;
private BigDecimal monthlyTurnover;
private Boolean isSeniorCitizen;
private Boolean isPwd;
private CodeValueData pwdCategory;
private Collection<CodeValueData> pwdCategoryOptions;
private BigDecimal agricultureIncome; private BigDecimal agricultureIncome;
private BigDecimal nonAgricultureIncome; private BigDecimal nonAgricultureIncome;
private Integer ageYears; private Integer ageYears;
@@ -400,13 +406,19 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
data.degreeQualificationOptions = templateData.degreeQualificationOptions; data.degreeQualificationOptions = templateData.degreeQualificationOptions;
data.religionOptions = templateData.religionOptions; data.religionOptions = templateData.religionOptions;
data.socialCategoryOptions = templateData.socialCategoryOptions; data.socialCategoryOptions = templateData.socialCategoryOptions;
data.pwdCategoryOptions = templateData.pwdCategoryOptions;
data.maritalStatus = clientData.maritalStatus; data.maritalStatus = clientData.maritalStatus;
data.educationLevel = clientData.educationLevel; data.educationLevel = clientData.educationLevel;
data.degreeQualification = clientData.degreeQualification; data.degreeQualification = clientData.degreeQualification;
data.religion = clientData.religion; data.religion = clientData.religion;
data.socialCategory = clientData.socialCategory; data.socialCategory = clientData.socialCategory;
data.monthlyIncome = clientData.monthlyIncome; data.monthlyIncome = clientData.monthlyIncome;
data.annualIncome = clientData.annualIncome;
data.annualTurnover = clientData.annualTurnover; data.annualTurnover = clientData.annualTurnover;
data.monthlyTurnover = clientData.monthlyTurnover;
data.isSeniorCitizen = clientData.isSeniorCitizen;
data.isPwd = clientData.isPwd;
data.pwdCategory = clientData.pwdCategory;
data.agricultureIncome = clientData.agricultureIncome; data.agricultureIncome = clientData.agricultureIncome;
data.nonAgricultureIncome = clientData.nonAgricultureIncome; data.nonAgricultureIncome = clientData.nonAgricultureIncome;
data.ageYears = clientData.ageYears; data.ageYears = clientData.ageYears;
@@ -456,13 +468,19 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
data.degreeQualificationOptions = clientData.degreeQualificationOptions; data.degreeQualificationOptions = clientData.degreeQualificationOptions;
data.religionOptions = clientData.religionOptions; data.religionOptions = clientData.religionOptions;
data.socialCategoryOptions = clientData.socialCategoryOptions; data.socialCategoryOptions = clientData.socialCategoryOptions;
data.pwdCategoryOptions = clientData.pwdCategoryOptions;
data.maritalStatus = clientData.maritalStatus; data.maritalStatus = clientData.maritalStatus;
data.educationLevel = clientData.educationLevel; data.educationLevel = clientData.educationLevel;
data.degreeQualification = clientData.degreeQualification; data.degreeQualification = clientData.degreeQualification;
data.religion = clientData.religion; data.religion = clientData.religion;
data.socialCategory = clientData.socialCategory; data.socialCategory = clientData.socialCategory;
data.monthlyIncome = clientData.monthlyIncome; data.monthlyIncome = clientData.monthlyIncome;
data.annualIncome = clientData.annualIncome;
data.annualTurnover = clientData.annualTurnover; data.annualTurnover = clientData.annualTurnover;
data.monthlyTurnover = clientData.monthlyTurnover;
data.isSeniorCitizen = clientData.isSeniorCitizen;
data.isPwd = clientData.isPwd;
data.pwdCategory = clientData.pwdCategory;
data.agricultureIncome = clientData.agricultureIncome; data.agricultureIncome = clientData.agricultureIncome;
data.nonAgricultureIncome = clientData.nonAgricultureIncome; data.nonAgricultureIncome = clientData.nonAgricultureIncome;
data.ageYears = clientData.ageYears; data.ageYears = clientData.ageYears;
@@ -509,7 +527,12 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
data.religion = clientData.religion; data.religion = clientData.religion;
data.socialCategory = clientData.socialCategory; data.socialCategory = clientData.socialCategory;
data.monthlyIncome = clientData.monthlyIncome; data.monthlyIncome = clientData.monthlyIncome;
data.annualIncome = clientData.annualIncome;
data.annualTurnover = clientData.annualTurnover; data.annualTurnover = clientData.annualTurnover;
data.monthlyTurnover = clientData.monthlyTurnover;
data.isSeniorCitizen = clientData.isSeniorCitizen;
data.isPwd = clientData.isPwd;
data.pwdCategory = clientData.pwdCategory;
data.agricultureIncome = clientData.agricultureIncome; data.agricultureIncome = clientData.agricultureIncome;
data.nonAgricultureIncome = clientData.nonAgricultureIncome; data.nonAgricultureIncome = clientData.nonAgricultureIncome;
data.ageYears = clientData.ageYears; data.ageYears = clientData.ageYears;
@@ -0,0 +1,35 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.data;
import java.io.Serializable;
import java.time.LocalDate;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public final class ClientFamilyMemberChildData implements Serializable {
private final Long id;
private final String fullName;
private final Long genderId;
private final String gender;
private final LocalDate dateOfBirth;
}
@@ -84,6 +84,9 @@ public final class ClientFamilyMembersData implements Serializable {
private final LocalDate marriageDate; private final LocalDate marriageDate;
private final String marriageAuthority; private final String marriageAuthority;
@lombok.Setter
private java.util.List<ClientFamilyMemberChildData> children;
// template holder // template holder
private final Collection<CodeValueData> relationshipIdOptions; private final Collection<CodeValueData> relationshipIdOptions;
private final Collection<CodeValueData> genderIdOptions; private final Collection<CodeValueData> genderIdOptions;
@@ -77,6 +77,9 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "image_id") @Column(name = "image_id")
private Long imageId; private Long imageId;
@Column(name = "signature_id")
private Long signatureId;
@Column(name = "status_enum", nullable = false) @Column(name = "status_enum", nullable = false)
private Integer status; private Integer status;
@@ -231,9 +234,25 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "monthly_income") @Column(name = "monthly_income")
private java.math.BigDecimal monthlyIncome; private java.math.BigDecimal monthlyIncome;
@Column(name = "annual_income")
private java.math.BigDecimal annualIncome;
@Column(name = "annual_turnover") @Column(name = "annual_turnover")
private java.math.BigDecimal annualTurnover; private java.math.BigDecimal annualTurnover;
@Column(name = "monthly_turnover")
private java.math.BigDecimal monthlyTurnover;
@Column(name = "is_senior_citizen")
private Boolean isSeniorCitizen;
@Column(name = "is_pwd")
private Boolean isPwd;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "pwd_category_cv_id")
private CodeValue pwdCategory;
@Column(name = "agriculture_income") @Column(name = "agriculture_income")
private java.math.BigDecimal agricultureIncome; private java.math.BigDecimal agricultureIncome;
@@ -598,6 +617,10 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return this.office.getId(); return this.office.getId();
} }
public String getDisplayName() {
return this.displayName;
}
public String mobileNo() { public String mobileNo() {
return this.mobileNo; return this.mobileNo;
} }
@@ -832,6 +855,14 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return this.socialCategory != null ? this.socialCategory.getId() : null; return this.socialCategory != null ? this.socialCategory.getId() : null;
} }
public Long pwdCategoryId() {
return this.pwdCategory != null ? this.pwdCategory.getId() : null;
}
public void updatePwdCategory(CodeValue pwdCategory) {
this.pwdCategory = pwdCategory;
}
public void updateSocialCategory(CodeValue socialCategory) { public void updateSocialCategory(CodeValue socialCategory) {
this.socialCategory = socialCategory; this.socialCategory = socialCategory;
} }
@@ -29,6 +29,7 @@ import lombok.Getter;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* Form 60 declaration as per Rule 114B of Income Tax Rules, 1962. * Form 60 declaration as per Rule 114B of Income Tax Rules, 1962.
@@ -87,7 +88,7 @@ public class ClientForm60 extends AbstractAuditableWithUTCDateTimeCustom<Long> {
form.estimatedTotalIncome = command.bigDecimalValueOfParameterNamed("estimatedTotalIncome"); form.estimatedTotalIncome = command.bigDecimalValueOfParameterNamed("estimatedTotalIncome");
form.reasonForNoPan = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamed("reasonForNoPan"), null); form.reasonForNoPan = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamed("reasonForNoPan"), null);
form.declaration = command.booleanPrimitiveValueOfParameterNamed("declaration"); form.declaration = command.booleanPrimitiveValueOfParameterNamed("declaration");
form.submittedOn = LocalDate.now(); form.submittedOn = DateUtils.getLocalDateOfTenant();
return form; return form;
} }
} }
@@ -32,6 +32,7 @@ import java.util.Map;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* Represents a client's KYC (Know Your Customer) record as mandated by the Reserve Bank of India * Represents a client's KYC (Know Your Customer) record as mandated by the Reserve Bank of India
@@ -260,7 +261,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.gstinVerified = false; this.gstinVerified = false;
this.cinVerified = false; this.cinVerified = false;
this.dinVerified = false; this.dinVerified = false;
this.initiatedOn = LocalDate.now(); this.initiatedOn = DateUtils.getLocalDateOfTenant();
} }
public static ClientKyc initiateKyc(final Client client, final JsonCommand command) { public static ClientKyc initiateKyc(final Client client, final JsonCommand command) {
@@ -456,13 +457,13 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
public void markCkycUploaded(final String ckycNumber, final String batchId) { public void markCkycUploaded(final String ckycNumber, final String batchId) {
this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null); this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null);
this.ckycBatchId = StringUtils.defaultIfEmpty(batchId, null); this.ckycBatchId = StringUtils.defaultIfEmpty(batchId, null);
this.ckycUploadedOn = LocalDate.now(); this.ckycUploadedOn = DateUtils.getLocalDateOfTenant();
} }
/** Called after successful 3rd-party verification of all required documents. */ /** Called after successful 3rd-party verification of all required documents. */
public void markVerified(final String ckycNumber) { public void markVerified(final String ckycNumber) {
this.kycStatus = ClientKycStatus.VERIFIED.getValue(); this.kycStatus = ClientKycStatus.VERIFIED.getValue();
this.verifiedOn = LocalDate.now(); this.verifiedOn = DateUtils.getLocalDateOfTenant();
this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null); this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null);
final KycRiskCategory risk = KycRiskCategory.fromInt(this.riskCategory); final KycRiskCategory risk = KycRiskCategory.fromInt(this.riskCategory);
@@ -474,14 +475,14 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
/** Called when verification fails. */ /** Called when verification fails. */
public void markRejected(final String remarks) { public void markRejected(final String remarks) {
this.kycStatus = ClientKycStatus.REJECTED.getValue(); this.kycStatus = ClientKycStatus.REJECTED.getValue();
this.rejectedOn = LocalDate.now(); this.rejectedOn = DateUtils.getLocalDateOfTenant();
this.remarks = StringUtils.defaultIfEmpty(remarks, this.remarks); this.remarks = StringUtils.defaultIfEmpty(remarks, this.remarks);
} }
public void markPanVerified(final String fullName, final String firstName, final String lastName, public void markPanVerified(final String fullName, final String firstName, final String lastName,
final String aadhaarSeedingStatus, final String category, final String status) { final String aadhaarSeedingStatus, final String category, final String status) {
this.panVerified = true; this.panVerified = true;
this.panVerifiedOn = LocalDate.now(); this.panVerifiedOn = DateUtils.getLocalDateOfTenant();
this.panFullName = StringUtils.defaultIfEmpty(fullName, null); this.panFullName = StringUtils.defaultIfEmpty(fullName, null);
this.panFirstName = StringUtils.defaultIfEmpty(firstName, null); this.panFirstName = StringUtils.defaultIfEmpty(firstName, null);
this.panLastName = StringUtils.defaultIfEmpty(lastName, null); this.panLastName = StringUtils.defaultIfEmpty(lastName, null);
@@ -507,28 +508,28 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
public void markAadhaarVerified() { public void markAadhaarVerified() {
this.aadhaarVerified = true; this.aadhaarVerified = true;
this.aadhaarVerifiedOn = LocalDate.now(); this.aadhaarVerifiedOn = DateUtils.getLocalDateOfTenant();
} }
public void markGstinVerified() { public void markGstinVerified() {
this.gstinVerified = true; this.gstinVerified = true;
this.gstinVerifiedOn = LocalDate.now(); this.gstinVerifiedOn = DateUtils.getLocalDateOfTenant();
} }
public void markCinVerified() { public void markCinVerified() {
this.cinVerified = true; this.cinVerified = true;
this.cinVerifiedOn = LocalDate.now(); this.cinVerifiedOn = DateUtils.getLocalDateOfTenant();
} }
public void markDinVerified() { public void markDinVerified() {
this.dinVerified = true; this.dinVerified = true;
this.dinVerifiedOn = LocalDate.now(); this.dinVerifiedOn = DateUtils.getLocalDateOfTenant();
} }
public void markDinVerified(final String dinNumber, final String mcaResponseJson) { public void markDinVerified(final String dinNumber, final String mcaResponseJson) {
this.dinNumber = dinNumber; this.dinNumber = dinNumber;
this.dinVerified = true; this.dinVerified = true;
this.dinVerifiedOn = LocalDate.now(); this.dinVerifiedOn = DateUtils.getLocalDateOfTenant();
this.isDirector = true; this.isDirector = true;
this.dinMcaResponse = mcaResponseJson; this.dinMcaResponse = mcaResponseJson;
} }
@@ -585,6 +586,15 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return riskCategory; return riskCategory;
} }
/**
* Directly sets the risk category outside the normal command-diff update path. Used by the AML
* module to auto-escalate a client's risk category after their 3rd cumulative STR case -
* every such change is recorded in {@code m_client_risk_category_history} by the caller.
*/
public void escalateRiskCategory(Integer newCategory) {
this.riskCategory = newCategory;
}
public String getPanNumber() { public String getPanNumber() {
return panNumber; return panNumber;
} }
@@ -55,6 +55,14 @@ public interface ClientRepository extends JpaRepository<Client, Long>, JpaSpecif
@Query("UPDATE Client client SET client.imageId = null WHERE client.id = :clientId") @Query("UPDATE Client client SET client.imageId = null WHERE client.id = :clientId")
void removeImageId(@Param("clientId") Long clientId); void removeImageId(@Param("clientId") Long clientId);
@Modifying
@Query("UPDATE Client client SET client.signatureId = :signatureId WHERE client.id = :clientId")
void updateByIdAndSignatureId(@Param("clientId") Long clientId, @Param("signatureId") Long signatureId);
@Modifying
@Query("UPDATE Client client SET client.signatureId = null WHERE client.id = :clientId")
void removeSignatureId(@Param("clientId") Long clientId);
/** /**
* Count clients whose cif_number begins with the given prefix. Used by the CIF number generator * Count clients whose cif_number begins with the given prefix. Used by the CIF number generator
* (TTYYYYNNNNNN format) to compute the next sequence within a given (type, year) bucket. * (TTYYYYNNNNNN format) to compute the next sequence within a given (type, year) bucket.
@@ -0,0 +1,173 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
@Entity
@Table(name = "m_client_risk_rule")
public class ClientRiskRule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "rule_name", nullable = false, length = 100)
private String ruleName;
@Column(name = "legal_form_id")
private Integer legalFormId;
@ManyToOne
@JoinColumn(name = "occupation_category_cv_id")
private CodeValue occupationCategory;
@ManyToOne
@JoinColumn(name = "occupation_cv_id")
private CodeValue occupation;
@Column(name = "min_monthly_income", precision = 19, scale = 6)
private BigDecimal minMonthlyIncome;
@Column(name = "max_monthly_income", precision = 19, scale = 6)
private BigDecimal maxMonthlyIncome;
@Column(name = "min_annual_income", precision = 19, scale = 6)
private BigDecimal minAnnualIncome;
@Column(name = "max_annual_income", precision = 19, scale = 6)
private BigDecimal maxAnnualIncome;
@Column(name = "min_annual_turnover", precision = 19, scale = 6)
private BigDecimal minAnnualTurnover;
@Column(name = "max_annual_turnover", precision = 19, scale = 6)
private BigDecimal maxAnnualTurnover;
@Column(name = "risk_category", nullable = false)
private Integer riskCategory;
@Column(name = "reason", nullable = false, length = 500)
private String reason;
@Column(name = "aml_check_status", length = 20)
private String amlCheckStatus;
@Column(name = "banned_entity_match", length = 10)
private String bannedEntityMatch;
@Column(name = "willful_defaulter", length = 10)
private String willfulDefaulter;
@Column(name = "ecgc_sal_flag", length = 10)
private String ecgcSalFlag;
@Column(name = "dormant_flag", length = 20)
private String dormantFlag;
@Column(name = "order_position", nullable = false)
private Integer orderPosition;
protected ClientRiskRule() {}
public Long getId() {
return id;
}
public String getRuleName() {
return ruleName;
}
public Integer getLegalFormId() {
return legalFormId;
}
public CodeValue getOccupationCategory() {
return occupationCategory;
}
public CodeValue getOccupation() {
return occupation;
}
public BigDecimal getMinMonthlyIncome() {
return minMonthlyIncome;
}
public BigDecimal getMaxMonthlyIncome() {
return maxMonthlyIncome;
}
public BigDecimal getMinAnnualIncome() {
return minAnnualIncome;
}
public BigDecimal getMaxAnnualIncome() {
return maxAnnualIncome;
}
public BigDecimal getMinAnnualTurnover() {
return minAnnualTurnover;
}
public BigDecimal getMaxAnnualTurnover() {
return maxAnnualTurnover;
}
public Integer getRiskCategory() {
return riskCategory;
}
public String getReason() {
return reason;
}
public Integer getOrderPosition() {
return orderPosition;
}
public String getAmlCheckStatus() {
return amlCheckStatus;
}
public String getBannedEntityMatch() {
return bannedEntityMatch;
}
public String getWillfulDefaulter() {
return willfulDefaulter;
}
public String getEcgcSalFlag() {
return ecgcSalFlag;
}
public String getDormantFlag() {
return dormantFlag;
}
}
@@ -0,0 +1,29 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
public interface ClientRiskRuleRepository extends JpaRepository<ClientRiskRule, Long> {
@Query("SELECT r FROM ClientRiskRule r ORDER BY r.orderPosition ASC")
List<ClientRiskRule> findAllOrderedByOrderPosition();
}
@@ -0,0 +1,54 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.currentaccount.data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
/**
* Read-side view of a single pending current account Nominee/Digital-Feature edit
* (m_current_account_pending_edit), for the Approvals -> Pending Current Account Edits screen.
*/
@Getter
@Setter
public class CurrentAccountPendingEditData implements Serializable {
private Long id;
private Long currentAccountId;
private String accountNo;
private Long makerId;
private String makerUsername;
private Long checkerId;
private String checkerUsername;
private LocalDateTime createdOn;
private LocalDateTime checkedOn;
private String status;
private String rejectReason;
private BigDecimal holdAmount;
private boolean facilityConfigSubmitted;
private boolean nomineeSubmitted;
private Map<String, Object> proposedFacilityConfig;
private Map<String, Object> proposedNominee;
public CurrentAccountPendingEditData() {}
}
@@ -24,6 +24,7 @@ import jakarta.persistence.Table;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import lombok.Getter; import lombok.Getter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* Activity log for each DIP / MNRL mobile number lookup request. * Activity log for each DIP / MNRL mobile number lookup request.
@@ -78,6 +79,6 @@ public class MobileLookupLog extends AbstractAuditableWithUTCDateTimeCustom<Long
this.rawResponse = rawResponse; this.rawResponse = rawResponse;
this.lookupSource = lookupSource; this.lookupSource = lookupSource;
this.clientId = clientId; this.clientId = clientId;
this.lookedUpOn = OffsetDateTime.now(); this.lookedUpOn = DateUtils.getOffsetDateTimeOfTenant();
} }
} }
@@ -18,7 +18,9 @@
*/ */
package org.apache.fineract.portfolio.sanctions.domain; package org.apache.fineract.portfolio.sanctions.domain;
import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
@@ -40,4 +42,11 @@ public interface SanctionsEntryRepository extends JpaRepository<SanctionsEntry,
void deleteAllByActiveTrue(); void deleteAllByActiveTrue();
long countByActiveTrue(); long countByActiveTrue();
/**
* Timestamp the active sanctions list was last loaded into the database — i.e. the data
* currency shown to callers so they know which list snapshot a screening ran against.
*/
@Query("SELECT MAX(s.createdDate) FROM SanctionsEntry s WHERE s.active = true")
Optional<OffsetDateTime> findListLastLoadedOn();
} }
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* Activity log for each sanctions screening request. * Activity log for each sanctions screening request.
@@ -73,7 +74,7 @@ public class SanctionsScreeningLog extends AbstractAuditableWithUTCDateTimeCusto
this.screeningSource = screeningSource; this.screeningSource = screeningSource;
this.clientId = clientId; this.clientId = clientId;
this.ipAddress = ipAddress; this.ipAddress = ipAddress;
this.screenedOn = OffsetDateTime.now(); this.screenedOn = DateUtils.getOffsetDateTimeOfTenant();
} }
// ── Accessors ───────────────────────────────────────────────────────────── // ── Accessors ─────────────────────────────────────────────────────────────
@@ -224,7 +224,7 @@ public final class DepositsApiConstants {
* Deposit Product Parameters * Deposit Product Parameters
*/ */
private static final Set<String> DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, private static final Set<String> DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName,
monthDayFormatParamName, nameParamName, shortNameParamName, descriptionParamName, currencyCodeParamName, dateFormatParamName, monthDayFormatParamName, nameParamName, shortNameParamName, descriptionParamName, currencyCodeParamName,
digitsAfterDecimalParamName, inMultiplesOfParamName, nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, digitsAfterDecimalParamName, inMultiplesOfParamName, nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName,
interestPostingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, interestPostingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName,
lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, accountingRuleParamName, chargesParamName, lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, accountingRuleParamName, chargesParamName,
@@ -512,6 +512,11 @@ public final class DepositsApiConstants {
fixedDepositInterestCalculationData.add(tenureInMonthsParamName); fixedDepositInterestCalculationData.add(tenureInMonthsParamName);
fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName); fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName);
fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName); fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName);
// Exact calendar-based calculation params
fixedDepositInterestCalculationData.add("submittedOnDate");
fixedDepositInterestCalculationData.add("dateFormat");
fixedDepositInterestCalculationData.add("locale");
fixedDepositInterestCalculationData.add("daysInYear");
return fixedDepositInterestCalculationData; return fixedDepositInterestCalculationData;
@@ -170,6 +170,7 @@ public final class SavingsAccountData implements Serializable {
private Boolean balanceSwapOption; private Boolean balanceSwapOption;
private Boolean msme; private Boolean msme;
private Boolean autoDebitFacility; private Boolean autoDebitFacility;
private Boolean terminateTrue;
private String avgBalanceCalcPeriod; private String avgBalanceCalcPeriod;
private String validForPeriod; private String validForPeriod;
private Boolean odAccountClosedZeroBalanceInDayClosing; private Boolean odAccountClosedZeroBalanceInDayClosing;
@@ -0,0 +1,55 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings.data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
/**
* Read-side view of a single pending savings account Nominee/Digital-Feature edit
* (m_savings_account_pending_edit), for the Approvals -> Pending Savings Account Edits screen.
*/
@Getter
@Setter
public class SavingsAccountPendingEditData implements Serializable {
private Long id;
private Long savingsAccountId;
private String accountNo;
private Long makerId;
private String makerUsername;
private Long checkerId;
private String checkerUsername;
private LocalDateTime createdOn;
private LocalDateTime checkedOn;
private String status;
private String rejectReason;
private BigDecimal holdAmount;
private boolean facilityConfigSubmitted;
private boolean nomineeSubmitted;
private Map<String, Object> proposedFacilityConfig;
private Map<String, Object> proposedNominee;
private String depositAccountType;
public SavingsAccountPendingEditData() {}
}
@@ -159,6 +159,9 @@ public final class SavingsProductData implements Serializable {
@Getter @Setter private Map<String, Object> depositLimitKyc; @Getter @Setter private Map<String, Object> depositLimitKyc;
@Getter @Setter private Map<String, List<Map<String, Object>>> otherGlMappingsMap; @Getter @Setter private Map<String, List<Map<String, Object>>> otherGlMappingsMap;
// True when a row exists in m_saving_product_temp with status = PENDING for this product - that row is the lock.
@Getter @Setter private Boolean isPendingApproval;
public static SavingsProductData template(final CurrencyData currency, final EnumOptionData interestCompoundingPeriodType, public static SavingsProductData template(final CurrencyData currency, final EnumOptionData interestCompoundingPeriodType,
final EnumOptionData interestPostingPeriodType, final EnumOptionData interestCalculationType, final EnumOptionData interestPostingPeriodType, final EnumOptionData interestCalculationType,
final EnumOptionData interestCalculationDaysInYearType, final EnumOptionData accountingRule, final EnumOptionData interestCalculationDaysInYearType, final EnumOptionData accountingRule,
@@ -692,6 +695,7 @@ public final class SavingsProductData implements Serializable {
} }
public boolean isCashBasedAccountingEnabled() { public boolean isCashBasedAccountingEnabled() {
if (this.accountingRule == null) return false;
return AccountingRuleType.CASH_BASED.getValue().toString().equals(this.accountingRule.getValue()); return AccountingRuleType.CASH_BASED.getValue().toString().equals(this.accountingRule.getValue());
} }
@@ -700,10 +704,12 @@ public final class SavingsProductData implements Serializable {
} }
public boolean isUpfrontAccrualAccounting() { public boolean isUpfrontAccrualAccounting() {
if (this.accountingRule == null) return false;
return AccountingRuleType.ACCRUAL_UPFRONT.getValue().toString().equals(this.accountingRule.getValue()); return AccountingRuleType.ACCRUAL_UPFRONT.getValue().toString().equals(this.accountingRule.getValue());
} }
public boolean isPeriodicAccrualAccounting() { public boolean isPeriodicAccrualAccounting() {
if (this.accountingRule == null) return false;
return AccountingRuleType.ACCRUAL_PERIODIC.getValue().toString().equals(this.accountingRule.getValue()); return AccountingRuleType.ACCRUAL_PERIODIC.getValue().toString().equals(this.accountingRule.getValue());
} }
@@ -0,0 +1,49 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings.data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
/**
* Read-side view of a single pending Savings Product edit row (m_savings_product_temp), for the Approvals ->
* Pending Savings Product Edits screen.
*/
@Getter
@Setter
public class SavingsProductTempData implements Serializable {
private Long id;
private Long savingsProductId;
private String savingsProductName;
private Long makerId;
private String makerUsername;
private Long checkerId;
private String checkerUsername;
private LocalDateTime createdOn;
private LocalDateTime checkedOn;
private String status;
private String rejectReason;
private Map<String, Object> proposedChanges;
public SavingsProductTempData() {}
}
@@ -36,7 +36,8 @@ public enum SavingsAccountStatusType {
REJECTED(500, "savingsAccountStatusType.rejected"), // REJECTED(500, "savingsAccountStatusType.rejected"), //
CLOSED(600, "savingsAccountStatusType.closed"), // CLOSED(600, "savingsAccountStatusType.closed"), //
PRE_MATURE_CLOSURE(700, "savingsAccountStatusType.pre.mature.closure"), // PRE_MATURE_CLOSURE(700, "savingsAccountStatusType.pre.mature.closure"), //
MATURED(800, "savingsAccountStatusType.matured"); // MATURED(800, "savingsAccountStatusType.matured"), //
TERMINATED(900, "savingsAccountStatusType.terminated"); //
private final Integer value; private final Integer value;
private final String code; private final String code;
@@ -53,6 +54,7 @@ public enum SavingsAccountStatusType {
case 600 -> SavingsAccountStatusType.CLOSED; case 600 -> SavingsAccountStatusType.CLOSED;
case 700 -> SavingsAccountStatusType.PRE_MATURE_CLOSURE; case 700 -> SavingsAccountStatusType.PRE_MATURE_CLOSURE;
case 800 -> SavingsAccountStatusType.MATURED; case 800 -> SavingsAccountStatusType.MATURED;
case 900 -> SavingsAccountStatusType.TERMINATED;
default -> SavingsAccountStatusType.INVALID; default -> SavingsAccountStatusType.INVALID;
}; };
} }
@@ -113,4 +115,8 @@ public enum SavingsAccountStatusType {
public boolean isPreMatureClosure() { public boolean isPreMatureClosure() {
return this.value.equals(SavingsAccountStatusType.PRE_MATURE_CLOSURE.getValue()); return this.value.equals(SavingsAccountStatusType.PRE_MATURE_CLOSURE.getValue());
} }
public boolean isTerminated() {
return this.value.equals(SavingsAccountStatusType.TERMINATED.getValue());
}
} }
@@ -285,6 +285,11 @@ public final class SavingsEnumerations {
SavingsAccountStatusType.MATURED.getCode(), "Matured", submittedAndPendingApproval, isApproved, isRejected, SavingsAccountStatusType.MATURED.getCode(), "Matured", submittedAndPendingApproval, isApproved, isRejected,
isWithdrawnByApplicant, isActive, isClosed, isPrematureClosed, isTransferInProgress, isTransferOnHold, matured); isWithdrawnByApplicant, isActive, isClosed, isPrematureClosed, isTransferInProgress, isTransferOnHold, matured);
break; break;
case TERMINATED:
optionData = new SavingsAccountStatusEnumData(SavingsAccountStatusType.TERMINATED.getValue().longValue(),
SavingsAccountStatusType.TERMINATED.getCode(), "Terminated", submittedAndPendingApproval, isApproved, isRejected,
isWithdrawnByApplicant, isActive, isClosed, isPrematureClosed, isTransferInProgress, isTransferOnHold, matured);
break;
} }
return optionData; return optionData;
} }
@@ -18,6 +18,7 @@
*/ */
package org.apache.fineract.portfolio.search.data; package org.apache.fineract.portfolio.search.data;
import java.util.Locale;
import org.apache.fineract.portfolio.search.SearchConstants.SearchSupportedResources; import org.apache.fineract.portfolio.search.SearchConstants.SearchSupportedResources;
public class SearchConditions { public class SearchConditions {
@@ -37,17 +38,17 @@ public class SearchConditions {
this.searchResource = searchResource; this.searchResource = searchResource;
this.exactMatch = exactMatch; this.exactMatch = exactMatch;
this.clientSearch = null == searchResource this.clientSearch = null == searchResource
|| searchResource.toLowerCase().contains(SearchSupportedResources.CLIENTS.name().toLowerCase()); || searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.CLIENTS.name().toLowerCase(Locale.ROOT));
this.groupSearch = null == searchResource this.groupSearch = null == searchResource
|| searchResource.toLowerCase().contains(SearchSupportedResources.GROUPS.name().toLowerCase()); || searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.GROUPS.name().toLowerCase(Locale.ROOT));
this.loanSeach = null == searchResource this.loanSeach = null == searchResource
|| searchResource.toLowerCase().contains(SearchSupportedResources.LOANS.name().toLowerCase()); || searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.LOANS.name().toLowerCase(Locale.ROOT));
this.savingSeach = null == searchResource this.savingSeach = null == searchResource
|| searchResource.toLowerCase().contains(SearchSupportedResources.SAVINGS.name().toLowerCase()); || searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.SAVINGS.name().toLowerCase(Locale.ROOT));
this.shareSeach = null == searchResource this.shareSeach = null == searchResource
|| searchResource.toLowerCase().contains(SearchSupportedResources.SHARES.name().toLowerCase()); || searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.SHARES.name().toLowerCase(Locale.ROOT));
this.clientIdentifierSearch = null == searchResource this.clientIdentifierSearch = null == searchResource
|| searchResource.toLowerCase().contains(SearchSupportedResources.CLIENTIDENTIFIERS.name().toLowerCase()); || searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.CLIENTIDENTIFIERS.name().toLowerCase(Locale.ROOT));
} }
public SearchConditions(final String searchQueryParam, final String searchResource, final Boolean clientSearch, public SearchConditions(final String searchQueryParam, final String searchResource, final Boolean clientSearch,
@@ -321,6 +321,6 @@ public class SearchUtil {
public String camelToSnake(final String camelStr) { public String camelToSnake(final String camelStr) {
return camelStr == null ? null return camelStr == null ? null
: camelStr.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2").replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase(); : camelStr.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2").replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase(Locale.ROOT);
} }
} }
@@ -0,0 +1,92 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.useradministration.agreement.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.useradministration.domain.AppUser;
/**
* A versioned, admin-manageable "user agreement" (bank policy/warning) that every user must accept right after
* logging in. Only one row may be {@link #active} at a time; enforced transactionally by the write service, not a
* DB constraint.
*/
@Entity
@Table(name = "m_user_agreement", uniqueConstraints = { @UniqueConstraint(columnNames = "version", name = "uq_user_agreement_version") })
@Getter
@Setter
@NoArgsConstructor
public class UserAgreement extends AbstractPersistableCustom<Long> {
@Column(name = "version", nullable = false)
private Integer version;
@Column(name = "title", length = 200, nullable = false)
private String title;
@Column(name = "body", nullable = false)
private String body;
@Column(name = "is_active", nullable = false)
private boolean active;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "created_by", nullable = false)
private AppUser createdBy;
@Column(name = "created_on", nullable = false, updatable = false)
private OffsetDateTime createdOn;
@PrePersist
private void prePersist() {
if (createdOn == null) {
createdOn = OffsetDateTime.now(ZoneOffset.UTC);
}
}
public static UserAgreement create(final Integer version, final String title, final String body, final AppUser createdBy) {
final UserAgreement agreement = new UserAgreement();
agreement.version = version;
agreement.title = title;
agreement.body = body;
agreement.createdBy = createdBy;
agreement.active = true;
return agreement;
}
public void activate() {
this.active = true;
}
public void deactivate() {
this.active = false;
}
}
@@ -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.useradministration.agreement.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.useradministration.domain.AppUser;
/**
* Audit-proof record of a user accepting a given version of the {@link UserAgreement} right after logging in.
* {@link #agreementVersion} is a denormalized snapshot of {@code UserAgreement.version} at acceptance time, so the
* acceptance record survives future version bumps unaffected.
*/
@Entity
@Table(name = "m_user_agreement_acceptance", uniqueConstraints = {
@UniqueConstraint(columnNames = { "appuser_id", "agreement_id" }, name = "uq_user_agreement_acceptance") })
@Getter
@Setter
@NoArgsConstructor
public class UserAgreementAcceptance extends AbstractPersistableCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "appuser_id", nullable = false)
private AppUser appUser;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "agreement_id", nullable = false)
private UserAgreement agreement;
@Column(name = "agreement_version", nullable = false)
private Integer agreementVersion;
@Column(name = "accepted_on", nullable = false, updatable = false)
private OffsetDateTime acceptedOn;
@Column(name = "ip_address", length = 45)
private String ipAddress;
@Column(name = "user_agent", length = 500)
private String userAgent;
@PrePersist
private void prePersist() {
if (acceptedOn == null) {
acceptedOn = OffsetDateTime.now(ZoneOffset.UTC);
}
}
public static UserAgreementAcceptance create(final AppUser appUser, final UserAgreement agreement, final String ipAddress,
final String userAgent) {
final UserAgreementAcceptance acceptance = new UserAgreementAcceptance();
acceptance.appUser = appUser;
acceptance.agreement = agreement;
acceptance.agreementVersion = agreement.getVersion();
acceptance.ipAddress = ipAddress;
acceptance.userAgent = userAgent;
return acceptance;
}
}
@@ -0,0 +1,26 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.useradministration.agreement.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserAgreementAcceptanceRepository extends JpaRepository<UserAgreementAcceptance, Long> {
boolean existsByAppUser_IdAndAgreement_Id(Long appUserId, Long agreementId);
}
@@ -0,0 +1,30 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.useradministration.agreement.domain;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserAgreementRepository extends JpaRepository<UserAgreement, Long> {
Optional<UserAgreement> findByActiveTrue();
List<UserAgreement> findAllByOrderByVersionDesc();
}
@@ -39,7 +39,7 @@ public interface BankUserDscRepository extends JpaRepository<BankUserDsc, Long>
/** /**
* Login gate: returns true if the user has at least one usable DSC mapping that's also not * Login gate: returns true if the user has at least one usable DSC mapping that's also not
* past its notAfter date. The {@code dscExpiryDate &gt;= today} clause prevents 2FA-gating a * past its notAfter date. The {@code dscExpiryDate >= today} clause prevents 2FA-gating a
* user who's enrolled but whose cert silently expired between log-ins — they'd otherwise be * user who's enrolled but whose cert silently expired between log-ins — they'd otherwise be
* stuck in a verification loop with a cert the helper would sign with but the server would * stuck in a verification loop with a cert the helper would sign with but the server would
* accept (the verify side filters too, but the gate is what kicks the user into the DSC flow * accept (the verify side filters too, but the gate is what kicks the user into the DSC flow
@@ -39,6 +39,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.staff.domain.Staff; import org.apache.fineract.organisation.staff.domain.Staff;
import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.useradministration.domain.AppUser;
@Entity @Entity
@Table(name = "m_current_account", uniqueConstraints = { @Table(name = "m_current_account", uniqueConstraints = {
@@ -102,6 +103,30 @@ public class CurrentAccount extends AbstractAuditableCustom {
@Column(name = "closedon_date") @Column(name = "closedon_date")
private LocalDate closedOnDate; private LocalDate closedOnDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "submittedon_userid")
private AppUser submittedOnByUser;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "approvedon_userid")
private AppUser approvedOnByUser;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "activatedon_userid")
private AppUser activatedOnByUser;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "rejectedon_userid")
private AppUser rejectedOnByUser;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "withdrawnon_userid")
private AppUser withdrawnOnByUser;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "closedon_userid")
private AppUser closedOnByUser;
// Currency (copied from product at account creation) // Currency (copied from product at account creation)
@Column(name = "currency_code", nullable = false, length = 3) @Column(name = "currency_code", nullable = false, length = 3)
private String currencyCode; private String currencyCode;
@@ -304,6 +329,12 @@ public class CurrentAccount extends AbstractAuditableCustom {
@Column(name = "od_account_closed_zero_balance_in_day_closing") @Column(name = "od_account_closed_zero_balance_in_day_closing")
private boolean odAccountClosedZeroBalanceInDayClosing = false; private boolean odAccountClosedZeroBalanceInDayClosing = false;
@Column(name = "is_linking_enable", nullable = false)
private boolean shareLinkingEnabled = false;
@Column(name = "terminate_true", nullable = false, columnDefinition = "boolean default false")
private boolean terminateTrue = false;
// Relationships // Relationships
@OneToMany(mappedBy = "currentAccount", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) @OneToMany(mappedBy = "currentAccount", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@@ -373,29 +404,34 @@ public class CurrentAccount extends AbstractAuditableCustom {
public void updateAccountNo(String accountNo) { this.accountNo = accountNo; } public void updateAccountNo(String accountNo) { this.accountNo = accountNo; }
public void approve(LocalDate approvedOnDate) { public void approve(LocalDate approvedOnDate, AppUser approvedOnByUser) {
this.statusEnum = 200; this.statusEnum = 200;
this.approvedOnDate = approvedOnDate; this.approvedOnDate = approvedOnDate;
this.approvedOnByUser = approvedOnByUser;
} }
public void activate(LocalDate activatedOnDate) { public void activate(LocalDate activatedOnDate, AppUser activatedOnByUser) {
this.statusEnum = 300; this.statusEnum = 300;
this.activatedOnDate = activatedOnDate; this.activatedOnDate = activatedOnDate;
this.activatedOnByUser = activatedOnByUser;
} }
public void reject(LocalDate rejectedOnDate) { public void reject(LocalDate rejectedOnDate, AppUser rejectedOnByUser) {
this.statusEnum = 500; this.statusEnum = 500;
this.rejectedOnDate = rejectedOnDate; this.rejectedOnDate = rejectedOnDate;
this.rejectedOnByUser = rejectedOnByUser;
} }
public void withdraw(LocalDate withdrawnOnDate) { public void withdraw(LocalDate withdrawnOnDate, AppUser withdrawnOnByUser) {
this.statusEnum = 400; this.statusEnum = 400;
this.withdrawnOnDate = withdrawnOnDate; this.withdrawnOnDate = withdrawnOnDate;
this.withdrawnOnByUser = withdrawnOnByUser;
} }
public void close(LocalDate closedOnDate) { public void close(LocalDate closedOnDate, AppUser closedOnByUser) {
this.statusEnum = 600; this.statusEnum = 600;
this.closedOnDate = closedOnDate; this.closedOnDate = closedOnDate;
this.closedOnByUser = closedOnByUser;
} }
public void setMinRequiredOpeningBalance(java.math.BigDecimal val) { this.minRequiredOpeningBalance = val; } public void setMinRequiredOpeningBalance(java.math.BigDecimal val) { this.minRequiredOpeningBalance = val; }
@@ -406,4 +442,15 @@ public class CurrentAccount extends AbstractAuditableCustom {
public void setNominalAnnualInterestRateOverdraft(java.math.BigDecimal val) { this.nominalAnnualInterestRateOverdraft = val; } public void setNominalAnnualInterestRateOverdraft(java.math.BigDecimal val) { this.nominalAnnualInterestRateOverdraft = val; }
public void setWithdrawalFeeForTransfer(boolean val) { this.withdrawalFeeForTransfer = val; } public void setWithdrawalFeeForTransfer(boolean val) { this.withdrawalFeeForTransfer = val; }
public void setMinOverdraftForInterestCalculation(java.math.BigDecimal val) { this.minOverdraftForInterestCalculation = val; } public void setMinOverdraftForInterestCalculation(java.math.BigDecimal val) { this.minOverdraftForInterestCalculation = val; }
public boolean isShareLinkingEnabled() { return shareLinkingEnabled; }
public void setShareLinkingEnabled(boolean shareLinkingEnabled) { this.shareLinkingEnabled = shareLinkingEnabled; }
public boolean isTerminateTrue() { return terminateTrue; }
public void setTerminateTrue(boolean terminateTrue) { this.terminateTrue = terminateTrue; }
public void terminate() {
this.terminateTrue = true;
this.statusEnum = 900;
}
} }
@@ -99,4 +99,31 @@ public class CurrentAccountFacilityConfig extends AbstractPersistableCustom<Long
cfg.currentAccount = account; cfg.currentAccount = account;
return cfg; return cfg;
} }
/**
* Partial update: only overwrites fields present (non-null) in the given JSON, leaving all others untouched.
* Mirrors {@code SavingsAccountFacilityConfig.updateFrom(JsonObject)}. Used by the Nominee/Digital-Feature
* edit-approval flow, which only ever submits the toggles the maker actually touched.
*/
public void updateFrom(com.google.gson.JsonObject config) {
if (config == null) return;
if (config.has("atmFacility")) setAtmFacility(config.get("atmFacility").getAsBoolean());
if (config.has("atmWithdrawalFacility")) setAtmWithdrawalFacility(config.get("atmWithdrawalFacility").getAsBoolean());
if (config.has("upiFacility")) setUpiFacility(config.get("upiFacility").getAsBoolean());
if (config.has("posTransactionsFacility")) setPosTransactionsFacility(config.get("posTransactionsFacility").getAsBoolean());
if (config.has("chequeBook")) setChequeBook(config.get("chequeBook").getAsBoolean());
if (config.has("impsFacility")) setImpsFacility(config.get("impsFacility").getAsBoolean());
if (config.has("smsAlerts")) setSmsAlerts(config.get("smsAlerts").getAsBoolean());
if (config.has("bbpsFacility")) setBbpsFacility(config.get("bbpsFacility").getAsBoolean());
if (config.has("insuranceCover")) setInsuranceCover(config.get("insuranceCover").getAsBoolean());
if (config.has("bepgFacility")) setBepgFacility(config.get("bepgFacility").getAsBoolean());
if (config.has("neftFacility")) setNeftFacility(config.get("neftFacility").getAsBoolean());
if (config.has("rtgsFacility")) setRtgsFacility(config.get("rtgsFacility").getAsBoolean());
if (config.has("interBankTotalCapFacility")) setInterBankTotalCapFacility(config.get("interBankTotalCapFacility").getAsBoolean());
if (config.has("withdrawalFacility")) setWithdrawalFacility(config.get("withdrawalFacility").getAsBoolean());
if (config.has("depositFacilityKyc")) setDepositFacilityKyc(config.get("depositFacilityKyc").getAsBoolean());
if (config.has("virtualDebitCard")) setVirtualDebitCard(config.get("virtualDebitCard").getAsBoolean());
if (config.has("internetBanking")) setInternetBanking(config.get("internetBanking").getAsBoolean());
if (config.has("mobileBanking")) setMobileBanking(config.get("mobileBanking").getAsBoolean());
}
} }
@@ -0,0 +1,35 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.currentaccount.domain;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* This repository did not previously exist - {@link CurrentAccountFacilityConfig} rows were only ever persisted via
* cascade from {@link CurrentAccountRepository} (see {@code CurrentAccount#facilityConfig}, {@code cascade = ALL}).
* Added so the Nominee/Digital-Feature edit-approval flow can save the config row directly, mirroring
* {@code SavingsAccountFacilityConfigRepository}.
*/
@Repository
public interface CurrentAccountFacilityConfigRepository extends JpaRepository<CurrentAccountFacilityConfig, Long> {
Optional<CurrentAccountFacilityConfig> findByCurrentAccountId(Long currentAccountId);
}
@@ -0,0 +1,597 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.currentaccount.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 java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.useradministration.domain.AppUser;
/**
* Holds a pending edit request for an ACTIVE current account's Nominee + Digital & Account Features.
* Presence of a row here with {@code status = PENDING} (enforced via the unique {@link #pendingLockKey}) is itself
* the lock - nothing is applied to the real {@code m_current_account_facility_config} / {@code m_client_nominee}
* rows until a checker approves (see CurrentAccountEditWritePlatformServiceJpaRepositoryImpl). Mirrors
* {@code SavingsAccountPendingEdit} but additionally carries a full point-in-time "wide snapshot" of every business
* column on {@link CurrentAccount} at submission time, for audit/UI display purposes only (not used to drive
* approve/reject logic, which reads the live account).
*/
@Entity
@Table(name = "m_current_account_pending_edit")
@Getter
@Setter
public class CurrentAccountPendingEdit extends AbstractPersistableCustom<Long> {
public static final String STATUS_PENDING = "PENDING";
public static final String STATUS_APPROVED = "APPROVED";
public static final String STATUS_REJECTED = "REJECTED";
public static final String NOMINEE_OP_ADD = "ADD";
public static final String NOMINEE_OP_UPDATE = "UPDATE";
public static final String NOMINEE_OP_DELETE = "DELETE";
@ManyToOne
@JoinColumn(name = "current_account_id", nullable = false)
private CurrentAccount currentAccount;
@ManyToOne
@JoinColumn(name = "createdby_id", nullable = false)
private AppUser maker;
@ManyToOne
@JoinColumn(name = "lastmodifiedby_id")
private AppUser checker;
@Column(name = "created_date", nullable = false)
private LocalDateTime createdOn;
@Column(name = "lastmodified_date")
private LocalDateTime checkedOn;
@Column(name = "status", nullable = false, length = 20)
private String status;
@Column(name = "reject_reason", length = 500)
private String rejectReason;
/** "CURRENT_ACCOUNT_EDIT:<currentAccountId>" while PENDING, NULL otherwise - the real DB-level lock. */
@Column(name = "pending_lock_key", length = 40)
private String pendingLockKey;
/** Links to the audit-only m_portfolio_command_source row created for this edit's submit/approve/reject. */
@Column(name = "command_source_id")
private Long commandSourceId;
@Column(name = "hold_transaction_id")
private Long holdTransactionId;
@Column(name = "hold_amount", precision = 19, scale = 6)
private BigDecimal holdAmount;
// ── Digital & Account Features (same names as m_current_account_facility_config) ──
@Column(name = "facility_config_submitted", nullable = false)
private boolean facilityConfigSubmitted = false;
@Column(name = "atm_facility")
private Boolean atmFacility;
@Column(name = "atm_withdrawal_facility")
private Boolean atmWithdrawalFacility;
@Column(name = "upi_facility")
private Boolean upiFacility;
@Column(name = "pos_transactions_facility")
private Boolean posTransactionsFacility;
@Column(name = "cheque_book")
private Boolean chequeBook;
@Column(name = "imps_facility")
private Boolean impsFacility;
@Column(name = "sms_alerts")
private Boolean smsAlerts;
@Column(name = "bbps_facility")
private Boolean bbpsFacility;
@Column(name = "insurance_cover")
private Boolean insuranceCover;
@Column(name = "bepg_facility")
private Boolean bepgFacility;
@Column(name = "neft_facility")
private Boolean neftFacility;
@Column(name = "rtgs_facility")
private Boolean rtgsFacility;
@Column(name = "inter_bank_total_cap_facility")
private Boolean interBankTotalCapFacility;
@Column(name = "withdrawal_facility")
private Boolean withdrawalFacility;
@Column(name = "deposit_facility_kyc")
private Boolean depositFacilityKyc;
@Column(name = "virtual_debit_card")
private Boolean virtualDebitCard;
@Column(name = "internet_banking")
private Boolean internetBanking;
@Column(name = "mobile_banking")
private Boolean mobileBanking;
// ── Nominee change - one per request (add / update / delete) ──
@Column(name = "nominee_submitted", nullable = false)
private boolean nomineeSubmitted = false;
@Column(name = "nominee_operation", length = 10)
private String nomineeOperation;
@Column(name = "nominee_id")
private Long nomineeId;
@Column(name = "nominee_client_id")
private Long nomineeClientId;
@Column(name = "relationship_cv_id")
private Long relationshipCvId;
@Column(name = "percentage_share", precision = 5, scale = 2)
private BigDecimal percentageShare;
@Column(name = "nominee_is_active")
private Boolean nomineeIsActive;
@Column(name = "nominee_is_minor")
private Boolean nomineeIsMinor;
@Column(name = "guardian_client_id")
private Long guardianClientId;
@Column(name = "guardian_relationship_cv_id")
private Long guardianRelationshipCvId;
@Column(name = "effective_date")
private LocalDate effectiveDate;
@Column(name = "nominee_remarks", length = 500)
private String nomineeRemarks;
// ── Wide point-in-time snapshot of every business column on CurrentAccount at submission time ──
// (audit/UI display only - approve()/reject() logic never reads these, it reads the live account)
@Column(name = "account_no", length = 20)
private String snapshotAccountNo;
@Column(name = "external_id", length = 100)
private String snapshotExternalId;
@Column(name = "client_id")
private Long snapshotClientId;
@Column(name = "product_id")
private Long snapshotProductId;
@Column(name = "office_id")
private Long snapshotOfficeId;
@Column(name = "staff_id")
private Long snapshotStaffId;
@Column(name = "status_enum")
private Integer snapshotStatusEnum;
@Column(name = "sub_status_enum")
private Integer snapshotSubStatusEnum;
@Column(name = "account_type_enum")
private Integer snapshotAccountTypeEnum;
@Column(name = "submittedon_date")
private LocalDate snapshotSubmittedOnDate;
@Column(name = "approvedon_date")
private LocalDate snapshotApprovedOnDate;
@Column(name = "activatedon_date")
private LocalDate snapshotActivatedOnDate;
@Column(name = "rejectedon_date")
private LocalDate snapshotRejectedOnDate;
@Column(name = "withdrawnon_date")
private LocalDate snapshotWithdrawnOnDate;
@Column(name = "closedon_date")
private LocalDate snapshotClosedOnDate;
@Column(name = "submittedon_userid")
private Long snapshotSubmittedOnUserId;
@Column(name = "approvedon_userid")
private Long snapshotApprovedOnUserId;
@Column(name = "activatedon_userid")
private Long snapshotActivatedOnUserId;
@Column(name = "rejectedon_userid")
private Long snapshotRejectedOnUserId;
@Column(name = "withdrawnon_userid")
private Long snapshotWithdrawnOnUserId;
@Column(name = "closedon_userid")
private Long snapshotClosedOnUserId;
@Column(name = "currency_code", length = 3)
private String snapshotCurrencyCode;
@Column(name = "currency_digits")
private Integer snapshotCurrencyDigits;
@Column(name = "currency_multiplesof")
private Integer snapshotCurrencyMultiplesOf;
@Column(name = "nominal_annual_interest_rate", precision = 19, scale = 6)
private BigDecimal snapshotNominalAnnualInterestRate;
@Column(name = "interest_compounding_period_enum")
private Integer snapshotInterestCompoundingPeriodEnum;
@Column(name = "interest_posting_period_enum")
private Integer snapshotInterestPostingPeriodEnum;
@Column(name = "interest_calculation_type_enum")
private Integer snapshotInterestCalculationTypeEnum;
@Column(name = "interest_calculation_days_in_year_type_enum")
private Integer snapshotInterestCalculationDaysInYearTypeEnum;
@Column(name = "min_balance_for_interest_calculation", precision = 19, scale = 6)
private BigDecimal snapshotMinBalanceForInterestCalculation;
@Column(name = "start_interest_calculation_date")
private LocalDate snapshotStartInterestCalculationDate;
@Column(name = "accrued_till_date")
private LocalDate snapshotAccruedTillDate;
@Column(name = "last_interest_calculation_date")
private LocalDate snapshotLastInterestCalculationDate;
@Column(name = "last_closed_business_date")
private LocalDate snapshotLastClosedBusinessDate;
@Column(name = "withhold_tax")
private Boolean snapshotWithholdTax;
@Column(name = "min_required_opening_balance", precision = 19, scale = 6)
private BigDecimal snapshotMinRequiredOpeningBalance;
@Column(name = "lockin_period_frequency")
private Integer snapshotLockinPeriodFrequency;
@Column(name = "lockin_period_frequency_enum")
private Integer snapshotLockinPeriodFrequencyEnum;
@Column(name = "lockedin_until_date_derived")
private LocalDate snapshotLockedInUntilDate;
@Column(name = "enforce_min_required_balance")
private Boolean snapshotEnforceMinRequiredBalance;
@Column(name = "min_required_balance", precision = 19, scale = 6)
private BigDecimal snapshotMinRequiredBalance;
@Column(name = "allow_overdraft")
private Boolean snapshotAllowOverdraft;
@Column(name = "overdraft_limit", precision = 19, scale = 6)
private BigDecimal snapshotOverdraftLimit;
@Column(name = "nominal_annual_interest_rate_overdraft", precision = 19, scale = 6)
private BigDecimal snapshotNominalAnnualInterestRateOverdraft;
@Column(name = "min_overdraft_for_interest_calculation", precision = 19, scale = 6)
private BigDecimal snapshotMinOverdraftForInterestCalculation;
@Column(name = "is_lien_allowed")
private Boolean snapshotLienAllowed;
@Column(name = "max_allowed_lien_limit", precision = 19, scale = 6)
private BigDecimal snapshotMaxAllowedLienLimit;
@Column(name = "on_hold_funds_derived", precision = 19, scale = 6)
private BigDecimal snapshotOnHoldFundsDerived;
@Column(name = "account_balance_derived", precision = 19, scale = 6)
private BigDecimal snapshotAccountBalanceDerived;
@Column(name = "total_deposits_derived", precision = 19, scale = 6)
private BigDecimal snapshotTotalDepositsDerived;
@Column(name = "total_withdrawals_derived", precision = 19, scale = 6)
private BigDecimal snapshotTotalWithdrawalsDerived;
@Column(name = "total_interest_earned_derived", precision = 19, scale = 6)
private BigDecimal snapshotTotalInterestEarnedDerived;
@Column(name = "total_interest_posted_derived", precision = 19, scale = 6)
private BigDecimal snapshotTotalInterestPostedDerived;
@Column(name = "total_overdraft_interest_derived", precision = 19, scale = 6)
private BigDecimal snapshotTotalOverdraftInterestDerived;
@Column(name = "total_withhold_tax_derived", precision = 19, scale = 6)
private BigDecimal snapshotTotalWithholdTaxDerived;
@Column(name = "total_savings_amount_on_hold", precision = 19, scale = 6)
private BigDecimal snapshotTotalSavingsAmountOnHold;
@Column(name = "withdrawal_fee_for_transfer")
private Boolean snapshotWithdrawalFeeForTransfer;
@Column(name = "reason_for_block", length = 500)
private String snapshotReasonForBlock;
@Column(name = "msme")
private Boolean snapshotMsme;
@Column(name = "auto_debit_facility")
private Boolean snapshotAutoDebitFacility;
@Column(name = "balance_swap_option")
private Boolean snapshotBalanceSwapOption;
@Column(name = "establishment_date")
private LocalDate snapshotEstablishmentDate;
@Column(name = "start_operation_date")
private LocalDate snapshotStartOperationDate;
@Column(name = "is_non_individual_client")
private Boolean snapshotNonIndividualClient;
@Column(name = "signatory_rights", length = 100)
private String snapshotSignatoryRights;
@Column(name = "cbs_account_type", length = 100)
private String snapshotCbsAccountType;
@Column(name = "customer_segment", length = 100)
private String snapshotCustomerSegment;
@Column(name = "account_status", length = 50)
private String snapshotAccountStatus;
@Column(name = "mode_of_operation", length = 50)
private String snapshotModeOfOperation;
@Column(name = "special_category", length = 100)
private String snapshotSpecialCategory;
@Column(name = "introducer_client_id")
private Long snapshotIntroducerClientId;
@Column(name = "introducer_name", length = 200)
private String snapshotIntroducerName;
@Column(name = "introducer_cif_number", length = 50)
private String snapshotIntroducerCifNumber;
@Column(name = "nominee_data", columnDefinition = "TEXT")
private String snapshotNomineeData;
@Column(name = "interest_calculate_selector")
private Integer snapshotInterestCalculateSelector;
@Column(name = "average_period", length = 20)
private String snapshotAveragePeriod;
@Column(name = "auto_post")
private Boolean snapshotAutoPost;
@Column(name = "frequency_fix_day")
private Integer snapshotFrequencyFixDay;
@Column(name = "enable_lockin_period")
private Boolean snapshotEnableLockinPeriod;
@Column(name = "balance_frequency", length = 50)
private String snapshotBalanceFrequency;
@Column(name = "avg_balance_calc_period", length = 10)
private String snapshotAvgBalanceCalcPeriod;
@Column(name = "account_closed_zero_balance_in_day_closing")
private Boolean snapshotAccountClosedZeroBalanceInDayClosing;
@Column(name = "valid_for_period", length = 10)
private String snapshotValidForPeriod;
@Column(name = "od_account_closed_zero_balance_in_day_closing")
private Boolean snapshotOdAccountClosedZeroBalanceInDayClosing;
@Column(name = "is_linking_enable")
private Boolean snapshotShareLinkingEnabled;
@Column(name = "terminate_true")
private Boolean snapshotTerminateTrue;
protected CurrentAccountPendingEdit() {}
public CurrentAccountPendingEdit(final CurrentAccount currentAccount, final AppUser maker) {
this.currentAccount = currentAccount;
this.maker = maker;
this.createdOn = DateUtils.getLocalDateTimeOfTenant();
this.status = STATUS_PENDING;
this.pendingLockKey = lockKeyFor(currentAccount.getId());
}
public static String lockKeyFor(final Long currentAccountId) {
return "CURRENT_ACCOUNT_EDIT:" + currentAccountId;
}
/**
* Builds a new pending-edit row and copies every business column off the live {@code account} onto this
* request's wide snapshot fields, for audit/UI display purposes only.
*/
public static CurrentAccountPendingEdit snapshotOf(final CurrentAccount account, final AppUser maker) {
final CurrentAccountPendingEdit p = new CurrentAccountPendingEdit(account, maker);
p.snapshotAccountNo = account.getAccountNo();
p.snapshotExternalId = account.getExternalId();
p.snapshotClientId = account.getClient() != null ? account.getClient().getId() : null;
p.snapshotProductId = account.getProductId();
p.snapshotOfficeId = account.getOffice() != null ? account.getOffice().getId() : null;
p.snapshotStaffId = account.getStaff() != null ? account.getStaff().getId() : null;
p.snapshotStatusEnum = account.getStatusEnum();
p.snapshotSubStatusEnum = account.getSubStatusEnum();
p.snapshotAccountTypeEnum = account.getAccountTypeEnum();
p.snapshotSubmittedOnDate = account.getSubmittedOnDate();
p.snapshotApprovedOnDate = account.getApprovedOnDate();
p.snapshotActivatedOnDate = account.getActivatedOnDate();
p.snapshotRejectedOnDate = account.getRejectedOnDate();
p.snapshotWithdrawnOnDate = account.getWithdrawnOnDate();
p.snapshotClosedOnDate = account.getClosedOnDate();
p.snapshotSubmittedOnUserId = account.getSubmittedOnByUser() != null ? account.getSubmittedOnByUser().getId() : null;
p.snapshotApprovedOnUserId = account.getApprovedOnByUser() != null ? account.getApprovedOnByUser().getId() : null;
p.snapshotActivatedOnUserId = account.getActivatedOnByUser() != null ? account.getActivatedOnByUser().getId() : null;
p.snapshotRejectedOnUserId = account.getRejectedOnByUser() != null ? account.getRejectedOnByUser().getId() : null;
p.snapshotWithdrawnOnUserId = account.getWithdrawnOnByUser() != null ? account.getWithdrawnOnByUser().getId() : null;
p.snapshotClosedOnUserId = account.getClosedOnByUser() != null ? account.getClosedOnByUser().getId() : null;
p.snapshotCurrencyCode = account.getCurrencyCode();
p.snapshotCurrencyDigits = account.getCurrencyDigits();
p.snapshotCurrencyMultiplesOf = account.getCurrencyMultiplesOf();
p.snapshotNominalAnnualInterestRate = account.getNominalAnnualInterestRate();
p.snapshotInterestCompoundingPeriodEnum = account.getInterestCompoundingPeriodEnum();
p.snapshotInterestPostingPeriodEnum = account.getInterestPostingPeriodEnum();
p.snapshotInterestCalculationTypeEnum = account.getInterestCalculationTypeEnum();
p.snapshotInterestCalculationDaysInYearTypeEnum = account.getInterestCalculationDaysInYearTypeEnum();
p.snapshotMinBalanceForInterestCalculation = account.getMinBalanceForInterestCalculation();
p.snapshotStartInterestCalculationDate = account.getStartInterestCalculationDate();
p.snapshotAccruedTillDate = account.getAccruedTillDate();
p.snapshotLastInterestCalculationDate = account.getLastInterestCalculationDate();
p.snapshotLastClosedBusinessDate = account.getLastClosedBusinessDate();
p.snapshotWithholdTax = account.isWithholdTax();
p.snapshotMinRequiredOpeningBalance = account.getMinRequiredOpeningBalance();
p.snapshotLockinPeriodFrequency = account.getLockinPeriodFrequency();
p.snapshotLockinPeriodFrequencyEnum = account.getLockinPeriodFrequencyEnum();
p.snapshotLockedInUntilDate = account.getLockedInUntilDate();
p.snapshotEnforceMinRequiredBalance = account.isEnforceMinRequiredBalance();
p.snapshotMinRequiredBalance = account.getMinRequiredBalance();
p.snapshotAllowOverdraft = account.isAllowOverdraft();
p.snapshotOverdraftLimit = account.getOverdraftLimit();
p.snapshotNominalAnnualInterestRateOverdraft = account.getNominalAnnualInterestRateOverdraft();
p.snapshotMinOverdraftForInterestCalculation = account.getMinOverdraftForInterestCalculation();
p.snapshotLienAllowed = account.isLienAllowed();
p.snapshotMaxAllowedLienLimit = account.getMaxAllowedLienLimit();
p.snapshotOnHoldFundsDerived = account.getOnHoldFundsDerived();
p.snapshotAccountBalanceDerived = account.getAccountBalanceDerived();
p.snapshotTotalDepositsDerived = account.getTotalDepositsDerived();
p.snapshotTotalWithdrawalsDerived = account.getTotalWithdrawalsDerived();
p.snapshotTotalInterestEarnedDerived = account.getTotalInterestEarnedDerived();
p.snapshotTotalInterestPostedDerived = account.getTotalInterestPostedDerived();
p.snapshotTotalOverdraftInterestDerived = account.getTotalOverdraftInterestDerived();
p.snapshotTotalWithholdTaxDerived = account.getTotalWithholdTaxDerived();
p.snapshotTotalSavingsAmountOnHold = account.getTotalSavingsAmountOnHold();
p.snapshotWithdrawalFeeForTransfer = account.isWithdrawalFeeForTransfer();
p.snapshotReasonForBlock = account.getReasonForBlock();
p.snapshotMsme = account.isMsme();
p.snapshotAutoDebitFacility = account.isAutoDebitFacility();
p.snapshotBalanceSwapOption = account.isBalanceSwapOption();
p.snapshotEstablishmentDate = account.getEstablishmentDate();
p.snapshotStartOperationDate = account.getStartOperationDate();
p.snapshotNonIndividualClient = account.isNonIndividualClient();
p.snapshotSignatoryRights = account.getSignatoryRights();
p.snapshotCbsAccountType = account.getCbsAccountType();
p.snapshotCustomerSegment = account.getCustomerSegment();
p.snapshotAccountStatus = account.getAccountStatus();
p.snapshotModeOfOperation = account.getModeOfOperation();
p.snapshotSpecialCategory = account.getSpecialCategory();
p.snapshotIntroducerClientId = account.getIntroducerClientId();
p.snapshotIntroducerName = account.getIntroducerName();
p.snapshotIntroducerCifNumber = account.getIntroducerCifNumber();
p.snapshotNomineeData = account.getNomineeData();
p.snapshotInterestCalculateSelector = account.getInterestCalculateSelector();
p.snapshotAveragePeriod = account.getAveragePeriod();
p.snapshotAutoPost = account.isAutoPost();
p.snapshotFrequencyFixDay = account.getFrequencyFixDay();
p.snapshotEnableLockinPeriod = account.isEnableLockinPeriod();
p.snapshotBalanceFrequency = account.getBalanceFrequency();
p.snapshotAvgBalanceCalcPeriod = account.getAvgBalanceCalcPeriod();
p.snapshotAccountClosedZeroBalanceInDayClosing = account.getAccountClosedZeroBalanceInDayClosing();
p.snapshotValidForPeriod = account.getValidForPeriod();
p.snapshotOdAccountClosedZeroBalanceInDayClosing = account.isOdAccountClosedZeroBalanceInDayClosing();
p.snapshotShareLinkingEnabled = account.isShareLinkingEnabled();
p.snapshotTerminateTrue = account.isTerminateTrue();
return p;
}
public boolean isPending() {
return STATUS_PENDING.equals(this.status);
}
public void markApproved(final AppUser checker) {
this.status = STATUS_APPROVED;
this.checker = checker;
this.checkedOn = DateUtils.getLocalDateTimeOfTenant();
this.pendingLockKey = null;
}
public void markRejected(final AppUser checker, final String reason) {
this.status = STATUS_REJECTED;
this.checker = checker;
this.checkedOn = DateUtils.getLocalDateTimeOfTenant();
this.rejectReason = reason;
this.pendingLockKey = null;
}
}
@@ -0,0 +1,32 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.currentaccount.domain;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CurrentAccountPendingEditRepository extends JpaRepository<CurrentAccountPendingEdit, Long> {
Optional<CurrentAccountPendingEdit> findByCurrentAccountIdAndStatus(Long currentAccountId, String status);
List<CurrentAccountPendingEdit> findByStatus(String status);
}
@@ -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.currentaccount.domain;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* <p>
* Wrapper for {@link CurrentAccountRepository} that is responsible for checking if {@link CurrentAccount} is
* returned when using <code>findOne</code> repository method and throwing an appropriate not found exception.
* </p>
*
* <p>
* This is to avoid need for checking and throwing in multiple areas of code base where
* {@link CurrentAccountRepository} is required.
* </p>
*/
@Service
@RequiredArgsConstructor
public class CurrentAccountRepositoryWrapper {
private final CurrentAccountRepository repository;
@Transactional(readOnly = true)
public CurrentAccount findOneWithNotFoundDetection(final Long id) {
return this.repository.findById(id).orElseThrow(() -> new CurrentAccountNotFoundException(id));
}
}
@@ -0,0 +1,37 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.currentaccount.exception;
import java.math.BigDecimal;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown at edit-submission time when the account's available balance cannot cover the charge for
* the digital/account feature(s) the maker is requesting to enable.
*/
public class CurrentAccountEditInsufficientBalanceException extends AbstractPlatformDomainRuleException {
public CurrentAccountEditInsufficientBalanceException(final Long currentAccountId, final BigDecimal availableBalance,
final BigDecimal requiredAmount) {
super("error.msg.currentaccount.edit.insufficient.balance",
"Account balance is insufficient for the requested feature(s). Available balance: " + availableBalance
+ ", required: " + requiredAmount,
currentAccountId, availableBalance, requiredAmount);
}
}
@@ -0,0 +1,30 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.currentaccount.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class CurrentAccountEditPendingApprovalException extends AbstractPlatformDomainRuleException {
public CurrentAccountEditPendingApprovalException(final Long currentAccountId) {
super("error.msg.currentaccount.edit.pending.approval.exists",
"Current account with identifier " + currentAccountId + " already has a Nominee/Digital-Feature edit pending approval",
currentAccountId);
}
}
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.currentaccount.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class CurrentAccountEditSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException {
public CurrentAccountEditSelfApprovalNotAllowedException(final Long currentAccountId) {
super("error.msg.currentaccount.edit.self.approval.not.allowed",
"The maker of the pending Nominee/Digital-Feature edit for current account " + currentAccountId
+ " cannot also approve or reject it",
currentAccountId);
}
}
@@ -0,0 +1,30 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.currentaccount.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class CurrentAccountNoPendingEditException extends AbstractPlatformDomainRuleException {
public CurrentAccountNoPendingEditException(final Long currentAccountId) {
super("error.msg.currentaccount.no.pending.edit",
"Current account with identifier " + currentAccountId + " has no Nominee/Digital-Feature edit awaiting approval",
currentAccountId);
}
}
@@ -0,0 +1,37 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.currentaccount.service;
import java.util.Collection;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.currentaccount.data.CurrentAccountPendingEditData;
public interface CurrentAccountEditWritePlatformService {
CommandProcessingResult submitEditForApproval(Long currentAccountId, JsonCommand command);
CommandProcessingResult approveEdit(Long currentAccountId);
CommandProcessingResult rejectEdit(Long currentAccountId, String rejectReason);
CurrentAccountPendingEditData retrievePendingEdit(Long currentAccountId);
Collection<CurrentAccountPendingEditData> retrieveAllPendingEdits();
}
@@ -37,6 +37,33 @@ public class CurrentAccountNumberGenerator {
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
private String getNextSequence() {
final java.util.List<String> accountNos = this.jdbcTemplate.query(
"SELECT account_no FROM m_current_account WHERE account_no IS NOT NULL",
(rs, rowNum) -> rs.getString(1));
long maxVal = 0;
for (String accNo : accountNos) {
if (accNo != null && accNo.length() >= 5) {
String suffix = accNo.substring(accNo.length() - 5);
try {
long val = Long.parseLong(suffix);
if (val > maxVal) {
maxVal = val;
}
} catch (NumberFormatException ignored) {
// non-numeric suffix - skip when computing the max existing sequence
}
}
}
String nextSeq = StringUtils.leftPad(String.valueOf(maxVal + 1), 5, '0');
if (nextSeq.length() > 5) {
nextSeq = nextSeq.substring(nextSeq.length() - 5);
}
return nextSeq;
}
public String generate(CurrentAccount account) { public String generate(CurrentAccount account) {
Long productId = account.getProductId(); Long productId = account.getProductId();
@@ -59,11 +86,10 @@ public class CurrentAccountNumberGenerator {
String officeId = StringUtils.leftPad(account.getOffice().getId().toString(), 2, '0'); String officeId = StringUtils.leftPad(account.getOffice().getId().toString(), 2, '0');
if (officeId.length() > 2) officeId = officeId.substring(officeId.length() - 2); if (officeId.length() > 2) officeId = officeId.substring(officeId.length() - 2);
// Account ID — 5 digits // Next sequence from existing account numbers
String accountId = StringUtils.leftPad(account.getId().toString(), 5, '0'); String nextSeq = getNextSequence();
if (accountId.length() > 5) accountId = accountId.substring(accountId.length() - 5);
return CBS_PREFIX + glCode + officeId + accountId; return CBS_PREFIX + glCode + officeId + nextSeq;
} }
public String preview(Long productId, Long officeId) { public String preview(Long productId, Long officeId) {
@@ -80,13 +106,10 @@ public class CurrentAccountNumberGenerator {
String officeStr = StringUtils.leftPad(officeId != null ? officeId.toString() : "0", 2, '0'); String officeStr = StringUtils.leftPad(officeId != null ? officeId.toString() : "0", 2, '0');
if (officeStr.length() > 2) officeStr = officeStr.substring(officeStr.length() - 2); if (officeStr.length() > 2) officeStr = officeStr.substring(officeStr.length() - 2);
// Next account ID estimate from m_current_account // Next sequence from existing account numbers
Long maxId = jdbcTemplate.query( String nextSeq = getNextSequence();
"SELECT MAX(id) FROM m_current_account",
rs -> rs.next() ? rs.getLong(1) : 0L);
String nextSeq = StringUtils.leftPad(String.valueOf((maxId != null ? maxId : 0L) + 1), 5, '0');
if (nextSeq.length() > 5) nextSeq = nextSeq.substring(nextSeq.length() - 5);
return CBS_PREFIX + glCode + officeStr + nextSeq; return CBS_PREFIX + glCode + officeStr + nextSeq;
} }
} }
@@ -26,6 +26,8 @@ public interface CurrentAccountReadPlatformService {
Map<String, Object> retrieveOne(Long accountId); Map<String, Object> retrieveOne(Long accountId);
Map<String, Object> retrieveOneTransaction(Long accountId, Long transactionId);
List<Map<String, Object>> retrieveAll(); List<Map<String, Object>> retrieveAll();
List<Map<String, Object>> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName); List<Map<String, Object>> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName);
@@ -18,6 +18,7 @@
*/ */
package org.apache.fineract.portfolio.currentaccount.service; package org.apache.fineract.portfolio.currentaccount.service;
import java.util.Set;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
@@ -40,4 +41,15 @@ public interface CurrentAccountWritePlatformService {
CommandProcessingResult modifyApplication(Long accountId, JsonCommand command); CommandProcessingResult modifyApplication(Long accountId, JsonCommand command);
CommandProcessingResult deleteApplication(Long accountId); CommandProcessingResult deleteApplication(Long accountId);
CommandProcessingResult terminateAccount(Long accountId, JsonCommand command);
/**
* Resolves each of the given (newly-enabled) facility types to its mapped charge via
* {@code m_facility_charge_mapping}, creates a {@code CurrentAccountCharge} for it (skipping any facility type
* for which the account already has a paid, non-waived charge for the same mapped {@code Charge}), collects it
* from the account's running balance as of today, and posts the corresponding GL journal entries. Reuses the
* same private charge-creation-and-collection helper that {@code activate()} uses for activation-time charges.
*/
void applyAndCollectFacilityCharges(Long currentAccountId, Set<String> newlyEnabledFacilityTypes);
} }
@@ -54,7 +54,6 @@ import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.MonthDay;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
@@ -76,7 +75,6 @@ import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.SavingsApiConstants;
import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType; import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType;
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYearType; import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYearType;
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType; import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct;
import org.apache.fineract.portfolio.savings.data.SavingsProductAccountingDataValidator; import org.apache.fineract.portfolio.savings.data.SavingsProductAccountingDataValidator;
@@ -56,18 +56,11 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountT
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.customerSegmentParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.customerSegmentParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.productCodeParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.productCodeParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.glAccountIdParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.glAccountIdParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.effectiveDateParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.effectiveToParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.endDateParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.introducerRequiredParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.introducerRequiredParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shareLinkingEnabledParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shareLinkingEnabledParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominationFacilityParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominationFacilityParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dormancyAlertParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dormancyAlertParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCalculateSelectorParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.averagePeriodParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.autoPostParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.autoPostParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.frequencyFixDayParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lastInterestDateParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enableLockinPeriodParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enableLockinPeriodParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.balanceSwapOptionParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.balanceSwapOptionParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.msmeParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.msmeParamName;
@@ -26,6 +26,7 @@ import jakarta.persistence.OneToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_current_product_deposit_limit_kyc") @Table(name = "m_current_product_deposit_limit_kyc")
@@ -57,12 +58,12 @@ public class CurrentProductDepositLimitKyc extends AbstractPersistableCustom<Lon
private LocalDateTime createdDate; private LocalDateTime createdDate;
protected CurrentProductDepositLimitKyc() { protected CurrentProductDepositLimitKyc() {
this.createdDate = LocalDateTime.now();
} }
public CurrentProductDepositLimitKyc(CurrentProduct currentProduct) { public CurrentProductDepositLimitKyc(CurrentProduct currentProduct) {
this(); this();
this.currentProduct = currentProduct; this.currentProduct = currentProduct;
this.createdDate = DateUtils.getLocalDateTimeOfTenant();
} }
public CurrentProduct getCurrentProduct() { return currentProduct; } public CurrentProduct getCurrentProduct() { return currentProduct; }
@@ -26,6 +26,7 @@ import jakarta.persistence.OneToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_current_product_facility_config") @Table(name = "m_current_product_facility_config")
@@ -87,12 +88,12 @@ public class CurrentProductFacilityConfig extends AbstractPersistableCustom<Long
private LocalDateTime createdDate; private LocalDateTime createdDate;
protected CurrentProductFacilityConfig() { protected CurrentProductFacilityConfig() {
this.createdDate = LocalDateTime.now();
} }
public CurrentProductFacilityConfig(CurrentProduct currentProduct) { public CurrentProductFacilityConfig(CurrentProduct currentProduct) {
this(); this();
this.currentProduct = currentProduct; this.currentProduct = currentProduct;
this.createdDate = DateUtils.getLocalDateTimeOfTenant();
} }
public CurrentProduct getCurrentProduct() { return currentProduct; } public CurrentProduct getCurrentProduct() { return currentProduct; }
@@ -26,6 +26,7 @@ import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_current_product_facility_limits") @Table(name = "m_current_product_facility_limits")
@@ -51,7 +52,6 @@ public class CurrentProductFacilityLimit extends AbstractPersistableCustom<Long>
private LocalDateTime createdDate; private LocalDateTime createdDate;
protected CurrentProductFacilityLimit() { protected CurrentProductFacilityLimit() {
this.createdDate = LocalDateTime.now();
} }
private CurrentProductFacilityLimit(CurrentProduct currentProduct, String limitType, private CurrentProductFacilityLimit(CurrentProduct currentProduct, String limitType,
@@ -62,6 +62,7 @@ public class CurrentProductFacilityLimit extends AbstractPersistableCustom<Long>
this.generalLimit = generalLimit; this.generalLimit = generalLimit;
this.minorAccountLimit = minorAccountLimit; this.minorAccountLimit = minorAccountLimit;
this.smallSavingLimit = smallSavingLimit; this.smallSavingLimit = smallSavingLimit;
this.createdDate = DateUtils.getLocalDateTimeOfTenant();
} }
public static CurrentProductFacilityLimit create(CurrentProduct currentProduct, String limitType, public static CurrentProductFacilityLimit create(CurrentProduct currentProduct, String limitType,
@@ -0,0 +1,29 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.currentproduct.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class CurrentProductEditPendingApprovalException extends AbstractPlatformDomainRuleException {
public CurrentProductEditPendingApprovalException(final Long productId) {
super("error.msg.currentproduct.edit.pending.approval",
"Current product with identifier " + productId + " already has an edit pending approval", productId);
}
}
@@ -0,0 +1,29 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.currentproduct.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class CurrentProductNoPendingEditException extends AbstractPlatformDomainRuleException {
public CurrentProductNoPendingEditException(final Long productId) {
super("error.msg.currentproduct.no.pending.edit",
"Current product with identifier " + productId + " has no edit pending approval", productId);
}
}
@@ -0,0 +1,29 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.currentproduct.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class CurrentProductSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException {
public CurrentProductSelfApprovalNotAllowedException(final Long productId) {
super("error.msg.currentproduct.self.approval.not.allowed",
"The maker of the pending edit for current product " + productId + " cannot also approve or reject it", productId);
}
}
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.currentproduct.service;
import java.util.Collection; import java.util.Collection;
import org.apache.fineract.portfolio.savings.data.SavingsProductData; import org.apache.fineract.portfolio.savings.data.SavingsProductData;
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
public interface CurrentProductReadPlatformService { public interface CurrentProductReadPlatformService {
@@ -28,4 +29,8 @@ public interface CurrentProductReadPlatformService {
Collection<SavingsProductData> retrieveAllForLookup(); Collection<SavingsProductData> retrieveAllForLookup();
SavingsProductData retrieveOne(Long productId); SavingsProductData retrieveOne(Long productId);
Collection<SavingsProductTempData> retrieveAllPendingEdits();
SavingsProductTempData retrievePendingEdit(Long productId);
} }
@@ -19,13 +19,11 @@
package org.apache.fineract.portfolio.currentproduct.service; package org.apache.fineract.portfolio.currentproduct.service;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -42,13 +40,23 @@ import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilit
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityConfigRepository; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityConfigRepository;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimit; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimit;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimitRepository; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimitRepository;
import org.apache.fineract.portfolio.currentproduct.exception.CurrentProductNoPendingEditException;
import org.apache.fineract.portfolio.savings.data.SavingsProductData; import org.apache.fineract.portfolio.savings.data.SavingsProductData;
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException; import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException;
import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; import org.apache.fineract.portfolio.savings.service.SavingsEnumerations;
import org.apache.fineract.portfolio.tax.data.TaxGroupData; import org.apache.fineract.portfolio.tax.data.TaxGroupData;
import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimitRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper;
import java.util.Set;
@RequiredArgsConstructor @RequiredArgsConstructor
public class CurrentProductReadPlatformServiceImpl implements CurrentProductReadPlatformService { public class CurrentProductReadPlatformServiceImpl implements CurrentProductReadPlatformService {
@@ -58,6 +66,11 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead
private final CurrentProductFacilityConfigRepository facilityConfigRepository; private final CurrentProductFacilityConfigRepository facilityConfigRepository;
private final CurrentProductFacilityLimitRepository facilityLimitRepository; private final CurrentProductFacilityLimitRepository facilityLimitRepository;
private final CurrentProductDepositLimitKycRepository depositLimitKycRepository; private final CurrentProductDepositLimitKycRepository depositLimitKycRepository;
private final ProductPendingEditRepository productPendingEditRepository;
private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository;
private final ProductPendingEditChargeRepository pendingEditChargeRepository;
private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository;
private final ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository;
private final CurrentProductMapper currentProductRowMapper = new CurrentProductMapper(); private final CurrentProductMapper currentProductRowMapper = new CurrentProductMapper();
private final CurrentProductLookupMapper currentProductLookupsRowMapper = new CurrentProductLookupMapper(); private final CurrentProductLookupMapper currentProductLookupsRowMapper = new CurrentProductLookupMapper();
@@ -147,6 +160,64 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead
} }
} }
private static final String PENDING_EDIT_SCHEMA = "spt.id as tempId, spt.product_id as savingsProductId, "
+ "cp.name as savingsProductName, spt.createdby_id as makerId, maker.username as makerUsername, "
+ "spt.lastmodifiedby_id as checkerId, checker.username as checkerUsername, spt.created_date as createdOn, "
+ "spt.lastmodified_date as checkedOn, spt.status as status, spt.reject_reason as rejectReason "
+ "from m_saving_product_temp spt "
+ "join m_current_product cp on cp.id = spt.product_id "
+ "join m_appuser maker on maker.id = spt.createdby_id "
+ "left join m_appuser checker on checker.id = spt.lastmodifiedby_id "
+ "where spt.product_type = 'CURRENT' ";
private RowMapper<SavingsProductTempData> getPendingEditRowMapper() {
return (rs, rowNum) -> {
final SavingsProductTempData data = new SavingsProductTempData();
data.setId(rs.getLong("tempId"));
data.setSavingsProductId(rs.getLong("savingsProductId"));
data.setSavingsProductName(rs.getString("savingsProductName"));
data.setMakerId(rs.getLong("makerId"));
data.setMakerUsername(rs.getString("makerUsername"));
data.setCheckerId(JdbcSupport.getLong(rs, "checkerId"));
data.setCheckerUsername(rs.getString("checkerUsername"));
data.setCreatedOn(rs.getTimestamp("createdOn") == null ? null : rs.getTimestamp("createdOn").toLocalDateTime());
data.setCheckedOn(rs.getTimestamp("checkedOn") == null ? null : rs.getTimestamp("checkedOn").toLocalDateTime());
data.setStatus(rs.getString("status"));
data.setRejectReason(rs.getString("rejectReason"));
final Long tempId = data.getId();
final java.util.Optional<ProductPendingEdit> opt = this.productPendingEditRepository.findById(tempId);
if (opt.isPresent()) {
final ProductPendingEdit pendingEdit = opt.get();
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final Map<String, Object> proposedChanges = ProductPendingEditFieldMapper.reconstructProposedChangesMap(
pendingEdit, submittedFields, this.pendingEditChargeRepository, this.pendingEditGlMappingRepository,
this.pendingEditFacilityLimitRepository, GSON);
data.setProposedChanges(proposedChanges);
}
return data;
};
}
@Override
public Collection<SavingsProductTempData> retrieveAllPendingEdits() {
this.context.authenticatedUser();
final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' order by spt.created_date desc";
return this.jdbcTemplate.query(sql, getPendingEditRowMapper());
}
@Override
public SavingsProductTempData retrievePendingEdit(final Long productId) {
this.context.authenticatedUser();
final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' and spt.product_id = ?";
try {
return this.jdbcTemplate.queryForObject(sql, getPendingEditRowMapper(), new Object[] { productId }); // NOSONAR
} catch (final EmptyResultDataAccessException e) {
throw new CurrentProductNoPendingEditException(productId);
}
}
private static final class CurrentProductMapper implements RowMapper<SavingsProductData> { private static final class CurrentProductMapper implements RowMapper<SavingsProductData> {
private final String schemaSql; private final String schemaSql;
@@ -197,7 +268,9 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead
sqlBuilder.append("cp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, "); sqlBuilder.append("cp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, ");
sqlBuilder.append("cp.balance_swap_option as balanceSwapOption, cp.msme as msme, cp.auto_debit_facility as autoDebitFacility, "); sqlBuilder.append("cp.balance_swap_option as balanceSwapOption, cp.msme as msme, cp.auto_debit_facility as autoDebitFacility, ");
sqlBuilder.append("cp.avg_balance_calc_period as avgBalanceCalcPeriod, cp.valid_for_period as validForPeriod, "); sqlBuilder.append("cp.avg_balance_calc_period as avgBalanceCalcPeriod, cp.valid_for_period as validForPeriod, ");
sqlBuilder.append("cp.od_account_closed_zero_balance_in_day_closing as odAccountClosedZeroBalanceInDayClosing "); sqlBuilder.append("cp.od_account_closed_zero_balance_in_day_closing as odAccountClosedZeroBalanceInDayClosing, ");
sqlBuilder.append("exists(select 1 from m_saving_product_temp spt where spt.product_id = cp.id "
+ "and spt.product_type = 'CURRENT' and spt.status = 'PENDING') as isPendingApproval ");
sqlBuilder.append("from m_current_product cp "); sqlBuilder.append("from m_current_product cp ");
sqlBuilder.append("join m_currency curr on curr.code = cp.currency_code "); sqlBuilder.append("join m_currency curr on curr.code = cp.currency_code ");
@@ -321,6 +394,7 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead
productData.setAvgBalanceCalcPeriod(rs.getString("avgBalanceCalcPeriod")); productData.setAvgBalanceCalcPeriod(rs.getString("avgBalanceCalcPeriod"));
productData.setValidForPeriod(rs.getString("validForPeriod")); productData.setValidForPeriod(rs.getString("validForPeriod"));
productData.setOdAccountClosedZeroBalanceInDayClosing(rs.getBoolean("odAccountClosedZeroBalanceInDayClosing")); productData.setOdAccountClosedZeroBalanceInDayClosing(rs.getBoolean("odAccountClosedZeroBalanceInDayClosing"));
productData.setIsPendingApproval(rs.getBoolean("isPendingApproval"));
return productData; return productData;
} }
@@ -28,4 +28,21 @@ public interface CurrentProductWritePlatformService {
CommandProcessingResult update(Long productId, JsonCommand command); CommandProcessingResult update(Long productId, JsonCommand command);
CommandProcessingResult delete(Long productId); CommandProcessingResult delete(Long productId);
/**
* Submits an edit for approval instead of applying it directly. Fails if a pending edit already exists for this
* product (that pending row is itself the lock).
*/
CommandProcessingResult submitEditForApproval(Long productId, JsonCommand command);
/**
* Replays the stored pending edit through the normal update path and marks it approved. Fails if the approving
* user is the same user who submitted the edit.
*/
CommandProcessingResult approveEdit(Long productId);
/**
* Discards the pending edit. Fails if the rejecting user is the same user who submitted the edit.
*/
CommandProcessingResult rejectEdit(Long productId, String rejectReason);
} }
@@ -31,6 +31,9 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService; import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService;
import org.apache.fineract.commands.domain.CommandProcessingResultType;
import org.apache.fineract.commands.domain.CommandSource;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
@@ -38,6 +41,8 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuild
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler; import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.Charge;
import org.apache.fineract.portfolio.currentproduct.data.CurrentProductDataValidator; import org.apache.fineract.portfolio.currentproduct.data.CurrentProductDataValidator;
@@ -50,10 +55,26 @@ import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilit
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimit; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimit;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimitRepository; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimitRepository;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository;
import org.apache.fineract.portfolio.currentproduct.exception.CurrentProductEditPendingApprovalException;
import org.apache.fineract.portfolio.currentproduct.exception.CurrentProductNoPendingEditException;
import org.apache.fineract.portfolio.currentproduct.exception.CurrentProductSelfApprovalNotAllowedException;
import org.apache.fineract.portfolio.savings.DepositsApiConstants; import org.apache.fineract.portfolio.savings.DepositsApiConstants;
import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException; import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedField;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditCharge;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimit;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimitRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMapping;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper;
import com.google.gson.JsonObject;
import org.apache.fineract.portfolio.tax.domain.TaxGroup; import org.apache.fineract.portfolio.tax.domain.TaxGroup;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -61,6 +82,7 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor @RequiredArgsConstructor
public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements CurrentProductWritePlatformService { public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements CurrentProductWritePlatformService {
private static final com.google.gson.Gson GSON = new com.google.gson.Gson();
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final CurrentProductRepository currentProductRepository; private final CurrentProductRepository currentProductRepository;
private final CurrentProductDataValidator fromApiJsonDataValidator; private final CurrentProductDataValidator fromApiJsonDataValidator;
@@ -69,6 +91,13 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr
private final CurrentProductFacilityConfigRepository facilityConfigRepository; private final CurrentProductFacilityConfigRepository facilityConfigRepository;
private final CurrentProductFacilityLimitRepository facilityLimitRepository; private final CurrentProductFacilityLimitRepository facilityLimitRepository;
private final CurrentProductDepositLimitKycRepository depositLimitKycRepository; private final CurrentProductDepositLimitKycRepository depositLimitKycRepository;
private final ProductPendingEditRepository productPendingEditRepository;
private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository;
private final ProductPendingEditChargeRepository pendingEditChargeRepository;
private final ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository;
private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository;
private final FromJsonHelper fromApiJsonHelper;
private final CommandSourceRepository commandSourceRepository;
private void handleDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dae) { private void handleDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dae) {
String msgCode = "error.msg." + DepositsApiConstants.CURRENT_PRODUCT_RESOURCE_NAME; String msgCode = "error.msg." + DepositsApiConstants.CURRENT_PRODUCT_RESOURCE_NAME;
@@ -251,6 +280,222 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr
} }
} }
@Transactional
@Override
public CommandProcessingResult submitEditForApproval(final Long productId, final JsonCommand command) {
final AppUser maker = this.context.authenticatedUser();
final CurrentProduct product = this.currentProductRepository.findById(productId)
.orElseThrow(() -> new SavingsProductNotFoundException(productId));
if (this.productPendingEditRepository
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_CURRENT, productId, ProductPendingEdit.STATUS_PENDING)
.isPresent()) {
throw new CurrentProductEditPendingApprovalException(productId);
}
this.fromApiJsonDataValidator.validateForUpdate(command.json(), product);
final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_CURRENT, productId, maker);
final java.util.Set<String> submitted = ProductPendingEditFieldMapper.populateCoreFields(command, pendingEdit.getCoreFields());
final java.util.Set<String> currentOnlySubmitted = ProductPendingEditFieldMapper.populateCurrentOnlyFields(command, pendingEdit.getCurrentOnlyFields());
final boolean facilityConfigSubmitted = ProductPendingEditFieldMapper.populateFacilityConfig(command, pendingEdit.getFacilityConfigFields());
final boolean depositLimitKycSubmitted = ProductPendingEditFieldMapper.populateDepositLimitKyc(command, pendingEdit.getDepositLimitKycFields());
final boolean glMappingSubmitted = ProductPendingEditFieldMapper.populateGlAccountMapping(command, pendingEdit.getGlAccountMappingFields());
if (!currentOnlySubmitted.isEmpty()) {
submitted.add("avgBalanceCalcPeriod");
submitted.add("validForPeriod");
submitted.add("odAccountClosedZeroBalanceInDayClosing");
}
if (facilityConfigSubmitted) {
submitted.add("accountFacilityConfig");
}
if (depositLimitKycSubmitted) {
submitted.add("depositLimitKyc");
}
if (glMappingSubmitted) {
submitted.add("glAccountMapping");
}
final CommandSource auditEntry = CommandSource.builder() //
.actionName("UPDATE") //
.entityName("CURRENTPRODUCT") //
.resourceGetUrl("/currentproducts/" + productId) //
.resourceId(productId) //
.commandAsJson(command.json()) //
.maker(maker) //
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
.idempotencyKey(java.util.UUID.randomUUID().toString()) //
.sanitized(false) //
.build();
this.commandSourceRepository.saveAndFlush(auditEntry);
pendingEdit.setCommandSourceId(auditEntry.getId());
this.productPendingEditRepository.saveAndFlush(pendingEdit);
for (final String fieldName : submitted) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, fieldName));
}
final List<Long> chargeIds = ProductPendingEditFieldMapper.extractChargeIds(command);
if (chargeIds != null) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "charges"));
for (final Long chargeId : chargeIds) {
this.pendingEditChargeRepository.save(new ProductPendingEditCharge(pendingEdit, chargeId));
}
}
final List<ProductPendingEditFacilityLimit> facilityLimits = ProductPendingEditFieldMapper.extractFacilityLimits(command, pendingEdit);
if (facilityLimits != null) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "accountFacilityLimits"));
this.pendingEditFacilityLimitRepository.saveAll(facilityLimits);
}
final List<ProductPendingEditGlMapping> glMappings = ProductPendingEditFieldMapper.extractGlArrayMappings(command, pendingEdit);
if (glMappings != null) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "glArrayMappings"));
this.pendingEditGlMappingRepository.saveAll(glMappings);
}
return new CommandProcessingResultBuilder() //
.withEntityId(product.getId()) //
.build();
}
@Transactional
@Override
public CommandProcessingResult approveEdit(final Long productId) {
final AppUser checker = this.context.authenticatedUser();
final ProductPendingEdit pendingEdit = this.productPendingEditRepository
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_CURRENT, productId, ProductPendingEdit.STATUS_PENDING)
.orElseThrow(() -> new CurrentProductNoPendingEditException(productId));
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
throw new CurrentProductSelfApprovalNotAllowedException(productId);
}
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final JsonObject root = new JsonObject();
root.addProperty("locale", "en");
root.addProperty("dateFormat", "yyyy-MM-dd");
ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields);
ProductPendingEditFieldMapper.writeCurrentOnlyFieldsToJson(root, pendingEdit.getCurrentOnlyFields(), submittedFields);
ProductPendingEditFieldMapper.writeFacilityConfigToJson(root, pendingEdit.getFacilityConfigFields(),
submittedFields.contains("accountFacilityConfig"));
ProductPendingEditFieldMapper.writeDepositLimitKycToJson(root, pendingEdit.getDepositLimitKycFields(),
submittedFields.contains("depositLimitKyc"));
if (submittedFields.contains("glAccountMapping")) {
ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields());
}
if (submittedFields.contains("charges")) {
ProductPendingEditFieldMapper.writeChargesToJson(root,
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("accountFacilityLimits")) {
ProductPendingEditFieldMapper.writeFacilityLimitsToJson(root,
this.pendingEditFacilityLimitRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("glArrayMappings")) {
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId()));
}
final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId, root, this.fromApiJsonHelper);
final CommandProcessingResult result = update(productId, replayedCommand);
pendingEdit.markApproved(checker);
this.productPendingEditRepository.saveAndFlush(pendingEdit);
final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId);
auditEntry.markAsChecked(checker);
auditEntry.updateForAudit(result);
this.commandSourceRepository.saveAndFlush(auditEntry);
return result;
}
private CommandSource findOrCreateAuditEntry(final ProductPendingEdit pendingEdit, final Long productId) {
if (pendingEdit.getCommandSourceId() != null) {
final java.util.Optional<CommandSource> existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId());
if (existing.isPresent()) {
return existing.get();
}
}
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final JsonObject root = new JsonObject();
root.addProperty("locale", "en");
root.addProperty("dateFormat", "yyyy-MM-dd");
ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields);
ProductPendingEditFieldMapper.writeCurrentOnlyFieldsToJson(root, pendingEdit.getCurrentOnlyFields(), submittedFields);
ProductPendingEditFieldMapper.writeFacilityConfigToJson(root, pendingEdit.getFacilityConfigFields(),
submittedFields.contains("accountFacilityConfig"));
ProductPendingEditFieldMapper.writeDepositLimitKycToJson(root, pendingEdit.getDepositLimitKycFields(),
submittedFields.contains("depositLimitKyc"));
if (submittedFields.contains("glAccountMapping")) {
ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields());
}
if (submittedFields.contains("charges")) {
ProductPendingEditFieldMapper.writeChargesToJson(root,
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("accountFacilityLimits")) {
ProductPendingEditFieldMapper.writeFacilityLimitsToJson(root,
this.pendingEditFacilityLimitRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("glArrayMappings")) {
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId()));
}
final String commandJson = GSON.toJson(root);
final CommandSource auditEntry = CommandSource.builder() //
.actionName("UPDATE") //
.entityName("CURRENTPRODUCT") //
.resourceGetUrl("/currentproducts/" + productId) //
.resourceId(productId) //
.commandAsJson(commandJson) //
.maker(pendingEdit.getMaker()) //
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
.idempotencyKey(java.util.UUID.randomUUID().toString()) //
.sanitized(false) //
.build();
this.commandSourceRepository.saveAndFlush(auditEntry);
pendingEdit.setCommandSourceId(auditEntry.getId());
this.productPendingEditRepository.saveAndFlush(pendingEdit);
return auditEntry;
}
@Transactional
@Override
public CommandProcessingResult rejectEdit(final Long productId, final String rejectReason) {
final AppUser checker = this.context.authenticatedUser();
final ProductPendingEdit pendingEdit = this.productPendingEditRepository
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_CURRENT, productId, ProductPendingEdit.STATUS_PENDING)
.orElseThrow(() -> new CurrentProductNoPendingEditException(productId));
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
throw new CurrentProductSelfApprovalNotAllowedException(productId);
}
pendingEdit.markRejected(checker, rejectReason);
this.productPendingEditRepository.saveAndFlush(pendingEdit);
final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId);
auditEntry.markAsRejected(checker);
this.commandSourceRepository.saveAndFlush(auditEntry);
return new CommandProcessingResultBuilder() //
.withEntityId(productId) //
.build();
}
@Transactional @Transactional
@Override @Override
public CommandProcessingResult delete(final Long productId) { public CommandProcessingResult delete(final Long productId) {
@@ -19,7 +19,14 @@
package org.apache.fineract.portfolio.currentproduct.starter; package org.apache.fineract.portfolio.currentproduct.starter;
import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService; import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService;
import org.apache.fineract.commands.domain.CommandSourceRepository;
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.savings.domain.ProductPendingEditRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimitRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.portfolio.currentproduct.data.CurrentProductDataValidator; import org.apache.fineract.portfolio.currentproduct.data.CurrentProductDataValidator;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductAssembler; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductAssembler;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductDepositLimitKycRepository; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductDepositLimitKycRepository;
@@ -43,9 +50,17 @@ public class CurrentProductConfiguration {
public CurrentProductReadPlatformService currentProductReadPlatformService(PlatformSecurityContext context, public CurrentProductReadPlatformService currentProductReadPlatformService(PlatformSecurityContext context,
JdbcTemplate jdbcTemplate, CurrentProductFacilityConfigRepository currentFacilityConfigRepository, JdbcTemplate jdbcTemplate, CurrentProductFacilityConfigRepository currentFacilityConfigRepository,
CurrentProductFacilityLimitRepository currentFacilityLimitRepository, CurrentProductFacilityLimitRepository currentFacilityLimitRepository,
CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository) { CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository,
ProductPendingEditRepository productPendingEditRepository,
ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository,
ProductPendingEditChargeRepository pendingEditChargeRepository,
ProductPendingEditGlMappingRepository pendingEditGlMappingRepository,
ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository) {
return new CurrentProductReadPlatformServiceImpl(context, jdbcTemplate, currentFacilityConfigRepository, return new CurrentProductReadPlatformServiceImpl(context, jdbcTemplate, currentFacilityConfigRepository,
currentFacilityLimitRepository, currentDepositLimitKycRepository); currentFacilityLimitRepository, currentDepositLimitKycRepository,
productPendingEditRepository, pendingEditSubmittedFieldRepository,
pendingEditChargeRepository, pendingEditGlMappingRepository,
pendingEditFacilityLimitRepository);
} }
@Bean @Bean
@@ -56,9 +71,18 @@ public class CurrentProductConfiguration {
ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService,
CurrentProductFacilityConfigRepository currentFacilityConfigRepository, CurrentProductFacilityConfigRepository currentFacilityConfigRepository,
CurrentProductFacilityLimitRepository currentFacilityLimitRepository, CurrentProductFacilityLimitRepository currentFacilityLimitRepository,
CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository) { CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository,
ProductPendingEditRepository productPendingEditRepository,
ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository,
ProductPendingEditChargeRepository pendingEditChargeRepository,
ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository,
ProductPendingEditGlMappingRepository pendingEditGlMappingRepository,
FromJsonHelper fromApiJsonHelper,
CommandSourceRepository commandSourceRepository) {
return new CurrentProductWritePlatformServiceJpaRepositoryImpl(context, currentProductRepository, currentProductDataValidator, return new CurrentProductWritePlatformServiceJpaRepositoryImpl(context, currentProductRepository, currentProductDataValidator,
currentProductAssembler, accountMappingWritePlatformService, currentFacilityConfigRepository, currentProductAssembler, accountMappingWritePlatformService, currentFacilityConfigRepository,
currentFacilityLimitRepository, currentDepositLimitKycRepository); currentFacilityLimitRepository, currentDepositLimitKycRepository, productPendingEditRepository,
pendingEditSubmittedFieldRepository, pendingEditChargeRepository, pendingEditFacilityLimitRepository,
pendingEditGlMappingRepository, fromApiJsonHelper, commandSourceRepository);
} }
} }

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