Compare commits

...
141 Commits
Author SHA1 Message Date
anshul 08519da8b1 feat: implement maker-checker workflow for user creation and update requests
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / 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 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-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-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-27 17:51:22 +05:30
Anshul Rathore cad7f54c47 feat: implement maker-checker for product creation, add app user preferences, and introduce base interest rate management services.
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 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 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
2026-08-26 10:38:09 +05:30
nitesh 8724c54d9b Merge branch 'dev' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend into dev
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract 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 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
# Conflicts:
#	fineract-provider/src/main/java/org/apache/fineract/portfolio/mobilelookup/service/MobileLookupServiceImpl.java
2026-08-19 10:50:25 +05:30
nitesh 5c0a482f49 feat(kyc): resolve DIP/MNRL demo verdicts locally, make the check toggleable
Every mobile-number lookup was calling out to a local mock HTTP service
on port 8090 that had to be started separately, so the check failed
with a connection error whenever that process wasn't running.

Ports the mock's full verdict table (suspected, disconnected,
suspected+disconnected, clean) into MobileLookupServiceImpl itself, so
the demo/test numbers resolve in-process with no HTTP call. Adds
fineract.mobile-lookup.enabled (default off) to skip the real DIP/MNRL
call entirely for local work; every number then resolves CLEAN and is
stamped LOOKUP_DISABLED_BY_CONFIG in the log so it's never mistaken
for a genuine pass. Must be turned back on before UAT/production.
2026-08-19 10:35:48 +05:30
nitesh 0e07e60328 feat(current-accounts): add document upload/list/download/delete endpoints
/v1/currentaccounts/{id}/documents had no handler at all -- the
account's own /v1/currentaccounts resource matches the request path
first and has no documents method, so it never fell through to the
generic /v1/{entityType}/{entityId}/documents resource that every
other entity type relies on. Current accounts got a 404 on every
Documents-tab request.

Delegates straight to the existing DocumentApiResource so current
accounts get identical behaviour to clients, loans, etc.
2026-08-19 10:35:35 +05:30
nitesh e227e8ae0f fix(savings): post GL entries for facility-charge collections at edit approval
Charges collected when a maker-checker Nominee/Digital-Feature edit is
approved deducted the account balance and appeared in the passbook,
but never posted a journal entry -- collectCharges() saved the charge
transaction directly and skipped the accounting bridge entirely, so
the GL never balanced against the subledger.

Also fixes an unrelated NPE this exposed: collectCharges() used
saveAll() instead of saveAllAndFlush(), so the new charge transaction
had no id yet when the journal-entry code tried to read it.
2026-08-19 10:35:18 +05:30
nitesh 231919b62b fix(savings): stop crediting accounts automatically on activation
Activation for Savings, RD, FD and Current accounts no longer books an
opening deposit or enforces a minimum opening balance -- accounts go
active at zero balance and a clerk deposits funds afterward as a
separate transaction, matching how these branches actually operate.

Fixed Deposit and Recurring Deposit have their own override of
activateWithBalance() that a prior fix to the shared SavingsAccount
base class missed, so they kept auto-depositing after Savings was
already fixed. Removing that auto-deposit exposed two follow-on bugs
in the FD/RD activation flow, now also fixed:
- the opening deposit's journal entries were posted twice (once by the
  deposit helper, once by the activation command's own posting call)
- FD/RD maturity amount was computed from the account's real (now
  empty) transaction history at activation and never recalculated once
  a deposit actually landed, so it showed no projected interest until
  the account was reloaded from a lucky code path
2026-08-19 10:34:54 +05:30
keshavandClaude Opus 5 9eac40bb5c feat: add database export API, welcome kit return tracking, and sanctions/mobile lookup updates
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-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-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 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
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:55:51 +05:30
niteshandClaude Sonnet 5 14b2585cf5 feat(loan-origination): add full backend persistence for the sanctioning pipeline
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Adds a lightweight loan-origination-pipeline module (application, stage history,
approvals, FI checks and documents) with server-side validation per stage
transition, so every field, upload and decision on the Loan Origination screen
survives a page refresh instead of living only in the browser.

Also fixes two content-store bugs found while wiring document uploads through
the generic Fineract document store: the CSV mime/extension whitelist rejected
bank-statement uploads outright, and the post-upload mime consistency check
rejected any CSV whose byte-content sniffs as text/plain (which is all of them,
since CSV has no distinguishing magic bytes).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 17:12:43 +05:30
Anshul Rathore a2fa8688ae Update client KYC and AML changes
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract 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 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-08-14 12:54:43 +05:30
keshav a8ca7dd948 feat: implement DIP/MNRL lookups and enhance Sanctions screening workflows
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - 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-5) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract 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
- Implement DIP/MNRL lookup logs table, filtering, and detail review under reports.
- Integrate DIP verification gating on client creation, allowing officers to record PROCEEDED/REJECTED decisions with reasons.
- Upgrade Sanctions verification dialog to support comparing Name, DOB, and Nationality particulars.
- Add Sanctions disposition recording (FALSE_POSITIVE / CONFIRMED_MATCH) awaiting Principal Officer approval.
- Add sanctions-alert-review-dialog for reviewers to assess and escalate/clear flagged names.
- Enforce strict screening reset logic on client name changes, preventing bypass of existing blocks.
- Update routes and shell menu navigation to include DIP/MNRL Lookups and Sanctions Logs.
2026-08-14 10:46:59 +05:30
niteshandClaude Sonnet 5 c8833367b9 feat(shares): route dividend cash claims through cashier token disbursement
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 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 - 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 E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Cash claims now mirror the existing clerk-withdrawal token flow: clerk
submits with a token, manager approval posts the GL entry, and a cashier
redeems the token at Transaction Entry to record physical hand-over --
no direct manager-to-member cash handover, and no double-disbursement.

Also fixes GL accounting for the loan/savings legs: instead of reversing
the native command's own debit with a second manual entry pair (leaving
the fund-source account confusingly appearing twice), that debit leg is
now rewritten in place to Dividend Payable. Floors cash claims to whole
rupees since notes/coins can't represent paise, with backward-compatible
tolerance for claims submitted before the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 10:17:35 +05:30
nitesh 94b0afff38 Merge branch 'dev' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend into dev
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract 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 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 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-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 Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
# Conflicts:
#	fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
2026-08-11 12:53:20 +05:30
niteshandClaude Sonnet 5 9dd4f8bedb feat(shares): real-profit driven dividends with GL-only liability model and member claim workflow
Reworks the share dividend lifecycle end to end:
- Dividend amount now auto-computed from real GL profit x officer-entered percentage
  instead of manual entry; adds a Dividend Paid report over the existing Dividend
  Payable ledger.
- Declaration posts a real GL entry (Dr Reserve for Dividend / Cr Dividend Payable);
  approval no longer credits any savings account up front -- every member's
  entitlement stays a pure "Dividend Payable" liability until claimed, resolved
  synchronously at approval time (no background job or per-member failure mode).
- New maker-checker Dividend Claim Request workflow: a clerk records how a member
  wants their unclaimed dividend split (overdue loan / savings credit / cash), a
  manager approves, which executes native repayment/deposit commands and replenishes
  whichever GL account they actually debited (not a hardcoded assumption) from the
  Dividend Payable liability.
- Unclaimed dividends carry a 3-year window (m_share_dividend_lock) after which a
  daily job forfeits them straight to profit; a Dividend Dashboard consolidates
  summary stats, every declaration's status, full lock history, and proactive
  loan-application for members with overdue EMIs.
- Fixes a native negative-share-days bug in ShareProductDividendAssembler and adds
  overlap-period validation so a product can't have two declarations covering the
  same days.
- Removes the now-unreachable failed/pending-payout tracking that the old
  per-member job-based model needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 12:42:45 +05:30
keshav 1107610c08 add KYC document renewal alerts, Aadhaar/PAN exceptions, and AML recurring alerts UI; remove old journal AML marking
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract 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 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-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
2026-08-11 11:34:54 +05:30
niteshandClaude Sonnet 5 2213308254 fix(shares): skip zero-value redeem transaction on account close
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-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 - 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 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
Closing a share account that already has zero approved shares no
longer synthesizes a redeem transaction, so it stops posting a
meaningless ₹0.00/₹0.00 GL journal-entry pair. Save/note-capture on
close now happen unconditionally instead of being gated on a
transaction existing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 20:10:57 +05:30
nitesh c568eb0abf feat(shares): close share account directly on withdrawal approval
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 - 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 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 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
Replaces the auto-submit-closure-request behavior with an immediate
account close, in the same @Transactional approval action, when a
redemption brings a share account's holding to zero — no separate
"Share Closure Request" maker-checker step is needed since the
redemption amount itself already went through this same review.
Removes ClerkShareClosureRequestApiResource entirely (the migration
for m_clerk_share_closure_request is left in place as a harmless,
now-unused table, to avoid a Liquibase checksum churn).

Also fixes a genuine Postgres-incompatibility bug this uncovered in
validateNoActiveLoanOrGuaranteeBeforeClosure(): `is_active = 1`
doesn't type-check against a boolean column on Postgres ("operator
does not exist: boolean = integer"), which was silently aborting the
whole approval transaction (harmlessly, since it's @Transactional —
verified no partial state was ever left behind). Fixed to `is_active
= true`, added the missing `type_enum = 1` (customer-type guarantors
only, matching the existing Guarantor Check elsewhere in this
workflow), and widened both this and the active-loan check to the
same loan_status_id set (300, 303, 304, 700) used there too.
2026-08-06 19:29:44 +05:30
nitesh 1426be9531 fix: correct changeset ids after renumbering merged AML/family-member migrations
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 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 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 E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 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
Follow-up to the previous merge commit — the internal Liquibase
changeSet ids in the 9 renumbered files (0447-0455) still referenced
their old 0438-0446 numbers; the rename staged the files before the id
update was applied. Ids now match filenames as intended.
2026-08-06 16:50:39 +05:30
nitesh 541c6ab062 Merge branch 'demo' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend into dev
Resolved two conflicts:
- changelog-tenant.xml: both branches independently added Liquibase
  parts numbered 0438-0446 (dev: Share Withdrawal/Closure governance +
  KYC exception fields; demo: AML rule tiers/branch seeding/email
  tracking/family-member children table). Renumbered demo's incoming
  set to 0447-0455 (files renamed + internal changeSet ids updated to
  match) so both feature sets keep their own migrations intact.
- 0434_seed_aml_transaction_rules.xml: same seed data on both sides,
  differing only in the disambiguating SQL predicate. Kept dev's
  `AND aml_check_status IS NOT NULL` (deterministically targets the
  intended m_client_risk_rule row, per the file's own comment) over
  demo's `LIMIT 1` (arbitrary, could pick the wrong row).

Also dropped two JVM crash-dump log files that got staged by mistake.
2026-08-06 16:49:15 +05:30
niteshandClaude Sonnet 5 66a6a4766f feat(shares): add Share Account Closure maker-checker workflow
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 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
New ClerkShareClosureRequestApiResource mirrors the withdrawal resource:
clerk submits a closure request only once a share account holds zero
shares; a /validate endpoint re-checks shares-held, account status, any
active loan/guarantee, and client KYC/AML compliance; approve() executes
the real native "close" command.

ClerkShareWithdrawalRequestApiResource.approve() now also auto-submits a
PENDING closure request (via the same table) whenever a redemption brings
the account's share holding to zero, reusing the withdrawal's own maker
identity — a manager still has to separately Validate and Approve it, so
the closure request is created automatically but never executed without
review.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 11:44:47 +05:30
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
nitesh 1a9adaec24 Merge branch 'dev' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend into dev
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 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 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 - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract 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-05 18:34:33 +05:30
niteshandClaude Sonnet 5 2da5a25e04 feat(shares): add compliance and loan/guarantor/tenure checks to validation
The /validate endpoint now re-checks live state that matters for a real
UCB share redemption: Client KYC verified, Risk Category not High, no open
AML alerts, and — since this bank supports loan-against-shares via
m_share_account.linked_loan_id — a Loan Check (no active loan secured by
these shares), Tenure Check (linked loan's term completed), and Guarantor
Check (client not guaranteeing someone else's active loan). Dropped the
redundant "status is PENDING" check and replaced raw status_enum codes
with human-readable labels throughout. getById() also now returns a
client compliance summary so the review screen can show it without
running the popup validation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 18:33:12 +05:30
keshav baf30cff68 kyc exception at client creation time and some tds work
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 Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - 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 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-08-05 17:27:56 +05:30
niteshandClaude Sonnet 5 f1d68d17fe feat(shares): add Share Withdrawal maker-checker workflow with GL settlement
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 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 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
New ClerkShareWithdrawalRequestApiResource backs a clerk-submits/manager-
approves flow for share redemption: on submit, validates and persists the
clerk-chosen settlement Payment Type (must be non-cash) plus a full audit
snapshot of the settlement row; on approve, executes the real "redeemshares"
command and, in the same transaction, credits the redemption value to the
share account's linked savings account using that stored payment type (with
a hardcoded-lookup fallback for pre-existing rows). Adds a /validate endpoint
that re-checks live account/GL state (not just the submit-time snapshot) so
a manager can verify before approving. Also exposes linked-savings-account
data on ShareAccount reads so the frontend can display/validate it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 16:56:50 +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 d7d22382a5 Merge branch 'demo' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend 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 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 - 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 E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-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
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2026-07-31 15:20:07 +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
niteshandClaude Sonnet 5 7cdc25322e fix(aml): resolve ambiguous order_position subquery breaking tenant migration/startup
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-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-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 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-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-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
0434_seed_aml_transaction_rules.xml resolved source_client_risk_rule_id via
`SELECT id FROM m_client_risk_rule WHERE order_position = N`, assuming
order_position was unique. It isn't: 0430_seed_aml_compliance_rules.xml seeds
its own 221 rows with order_position 1-235, independently of the older
0428_add_client_risk_rules.xml rows, and several values collide (e.g. both
insert a row at order_position 10 and 30). Every affected subquery returned
more than one row, aborting the whole tenant's Liquibase run and app startup.

aml_check_status is populated only on 0430's rows (0428's predate that
column and are NULL there), so pairing it with order_position uniquely
targets the intended row without touching either seed changeset.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 14:27:08 +05:30
nitesh a4c3dbb6c4 Merge remote-tracking branch 'origin/dev' into uate
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract 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 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 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-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
2026-07-31 13:58:55 +05:30
niteshandClaude Sonnet 5 dd8445de06 fix(accounting): resolve Sponsor DD Payable SBI GL account parent by gl_code, update default tenant DB password
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract 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 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 - 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
The 0345 changelog hardcoded the "OTHER LIABILITY" parent row's numeric id
(22), which only matched the default tenant's auto-increment history and
broke the FK insert (and the whole Liquibase run / app startup) on tenants
seeded differently, e.g. saga. Now resolves parent_id via a SELECT on
gl_code='9' and marks the changeSet runOnChange so it safely re-validates
on tenants that already ran the old version.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 13:56:04 +05:30
nitesh 9345f2294c Merge remote-tracking branch 'origin/uate' into uate
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 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 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 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract 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
2026-07-31 13:54:07 +05:30
niteshandClaude Sonnet 5 190cd92a85 fix(accounting): resolve Sponsor DD Payable SBI GL account parent by gl_code, update default tenant DB password
The 0345 changelog hardcoded the "OTHER LIABILITY" parent row's numeric id
(22), which only matched the default tenant's auto-increment history and
broke the FK insert (and the whole Liquibase run / app startup) on tenants
seeded differently, e.g. saga. Now resolves parent_id via a SELECT on
gl_code='9' and marks the changeSet runOnChange so it safely re-validates
on tenants that already ran the old version.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 13:53:27 +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
1294 changed files with 123384 additions and 384434 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",
+4 -1
View File
@@ -34,4 +34,7 @@ gradleExp/
.run/ .run/
.java-version .java-version
.windsurf/ .windsurf/
# Application logs
fineract-provider/logs/
+1
View File
@@ -0,0 +1 @@
{"username":"mifos", "password":"password"}
+1 -1
View File
@@ -159,7 +159,7 @@ gitVersioning.apply {
} }
ext['groovy.version'] = '4.0.17' ext['groovy.version'] = '4.0.17'
ext['swaggerFile'] = "$rootDir/fineract-provider/build/resources/main/static/fineract.json".toString() ext['swaggerFile'] = "$rootDir/fineract-provider/build/resources/main/static/fineract.json".toString().replace(" ", "%20")
// Apply static weaving configuration to all subprojects // Apply static weaving configuration to all subprojects
subprojects { subproject -> subprojects { subproject ->
+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);
@@ -1,272 +1,284 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.accounting.glaccount.service; package org.apache.fineract.accounting.glaccount.service;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.common.AccountingEnumerations; import org.apache.fineract.accounting.common.AccountingEnumerations;
import org.apache.fineract.accounting.glaccount.data.GLAccountData; import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.accounting.glaccount.data.GLAccountDataForLookup; import org.apache.fineract.accounting.glaccount.data.GLAccountDataForLookup;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType; import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.glaccount.domain.GLAccountUsage; import org.apache.fineract.accounting.glaccount.domain.GLAccountUsage;
import org.apache.fineract.accounting.glaccount.exception.GLAccountInvalidClassificationException; import org.apache.fineract.accounting.glaccount.exception.GLAccountInvalidClassificationException;
import org.apache.fineract.accounting.glaccount.exception.GLAccountNotFoundException; import org.apache.fineract.accounting.glaccount.exception.GLAccountNotFoundException;
import org.apache.fineract.accounting.journalentry.data.JournalEntryAssociationParametersData; import org.apache.fineract.accounting.journalentry.data.JournalEntryAssociationParametersData;
import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport; import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
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.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformService { public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformService {
private static final String NAME_DECORATED_BASE_ON_HIERARCHY = "concat(substring('........................................', 1, ((LENGTH(hierarchy) - LENGTH(REPLACE(hierarchy, '.', '')) - 1) * 4)), name)"; private static final String NAME_DECORATED_BASE_ON_HIERARCHY = "concat(substring('........................................', 1, ((LENGTH(hierarchy) - LENGTH(REPLACE(hierarchy, '.', '')) - 1) * 4)), name)";
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
private static final String SQL_AND = " and "; private static final String SQL_AND = " and ";
private static final class GLAccountMapper implements RowMapper<GLAccountData> { private static final class GLAccountMapper implements RowMapper<GLAccountData> {
private final JournalEntryAssociationParametersData associationParametersData; private final JournalEntryAssociationParametersData associationParametersData;
GLAccountMapper(final JournalEntryAssociationParametersData associationParametersData) { GLAccountMapper(final JournalEntryAssociationParametersData associationParametersData) {
if (associationParametersData == null) { if (associationParametersData == null) {
this.associationParametersData = new JournalEntryAssociationParametersData(); this.associationParametersData = new JournalEntryAssociationParametersData();
} else { } else {
this.associationParametersData = associationParametersData; this.associationParametersData = associationParametersData;
} }
} }
public String schema() { public String schema() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append( sb.append(
" gl.id as id, name as name, parent_id as parentId, gl_code as glCode, disabled as disabled, manual_journal_entries_allowed as manualEntriesAllowed, ") " gl.id as id, name as name, parent_id as parentId, gl_code as glCode, disabled as disabled, manual_journal_entries_allowed as manualEntriesAllowed, ")
.append("classification_enum as classification, account_usage as accountUsage, gl.description as description, ") .append("classification_enum as classification, account_usage as accountUsage, gl.description as description, ")
.append(NAME_DECORATED_BASE_ON_HIERARCHY).append(" as nameDecorated, ") .append(NAME_DECORATED_BASE_ON_HIERARCHY).append(" as nameDecorated, ")
.append("cv.id as codeId, cv.code_value as codeValue "); .append("cv.id as codeId, cv.code_value as codeValue ");
if (this.associationParametersData.isRunningBalanceRequired()) { if (this.associationParametersData.isRunningBalanceRequired()) {
sb.append(",gl_j.organization_running_balance as organizationRunningBalance "); sb.append(",gl_j.organization_running_balance as organizationRunningBalance ");
} }
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 ")
return sb.toString(); .append("where account_id = gl.id ")
} .append("order by id desc limit 1) ");
}
@Override return sb.toString();
public GLAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { }
final Long id = rs.getLong("id"); @Override
final String name = rs.getString("name"); public GLAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long parentId = JdbcSupport.getLong(rs, "parentId");
final String glCode = rs.getString("glCode"); final Long id = rs.getLong("id");
final boolean disabled = rs.getBoolean("disabled"); final String name = rs.getString("name");
final boolean manualEntriesAllowed = rs.getBoolean("manualEntriesAllowed"); final Long parentId = JdbcSupport.getLong(rs, "parentId");
final int accountTypeId = JdbcSupport.getInteger(rs, "classification"); final String glCode = rs.getString("glCode");
final EnumOptionData accountType = AccountingEnumerations.gLAccountType(accountTypeId); final boolean disabled = rs.getBoolean("disabled");
final int usageId = JdbcSupport.getInteger(rs, "accountUsage"); final boolean manualEntriesAllowed = rs.getBoolean("manualEntriesAllowed");
final EnumOptionData usage = AccountingEnumerations.gLAccountUsage(usageId); final int accountTypeId = JdbcSupport.getInteger(rs, "classification");
final String description = rs.getString("description"); final EnumOptionData accountType = AccountingEnumerations.gLAccountType(accountTypeId);
final String nameDecorated = rs.getString("nameDecorated"); final int usageId = JdbcSupport.getInteger(rs, "accountUsage");
final Long codeId = rs.wasNull() ? null : rs.getLong("codeId"); final EnumOptionData usage = AccountingEnumerations.gLAccountUsage(usageId);
final String codeValue = rs.getString("codeValue"); final String description = rs.getString("description");
final CodeValueData tagId = CodeValueData.instance(codeId, codeValue); final String nameDecorated = rs.getString("nameDecorated");
Long organizationRunningBalance = null; final Long codeId = rs.wasNull() ? null : rs.getLong("codeId");
if (associationParametersData.isRunningBalanceRequired()) { final String codeValue = rs.getString("codeValue");
organizationRunningBalance = rs.getLong("organizationRunningBalance"); final CodeValueData tagId = CodeValueData.instance(codeId, codeValue);
} Long organizationRunningBalance = null;
return new GLAccountData().setId(id).setName(name).setParentId(parentId).setGlCode(glCode).setDisabled(disabled) if (associationParametersData.isRunningBalanceRequired()) {
.setManualEntriesAllowed(manualEntriesAllowed).setType(accountType).setUsage(usage).setDescription(description) organizationRunningBalance = rs.getLong("organizationRunningBalance");
.setNameDecorated(nameDecorated).setTagId(tagId).setOrganizationRunningBalance(organizationRunningBalance); }
} return new GLAccountData().setId(id).setName(name).setParentId(parentId).setGlCode(glCode).setDisabled(disabled)
} .setManualEntriesAllowed(manualEntriesAllowed).setType(accountType).setUsage(usage).setDescription(description)
.setNameDecorated(nameDecorated).setTagId(tagId).setOrganizationRunningBalance(organizationRunningBalance);
@Override }
public List<GLAccountData> retrieveAllGLAccounts(final Integer accountClassification, final String searchParam, final Integer usage, }
final Boolean manualTransactionsAllowed, final Boolean disabled,
JournalEntryAssociationParametersData associationParametersData) { @Override
if (accountClassification != null && !checkValidGLAccountType(accountClassification)) { public List<GLAccountData> retrieveAllGLAccounts(final Integer accountClassification, final String searchParam, final Integer usage,
throw new GLAccountInvalidClassificationException(accountClassification); final Boolean manualTransactionsAllowed, final Boolean disabled,
} JournalEntryAssociationParametersData associationParametersData) {
if (accountClassification != null && !checkValidGLAccountType(accountClassification)) {
if (usage != null && !checkValidGLAccountUsage(usage)) { throw new GLAccountInvalidClassificationException(accountClassification);
throw new GLAccountInvalidClassificationException(accountClassification); }
}
if (usage != null && !checkValidGLAccountUsage(usage)) {
final GLAccountMapper rm = new GLAccountMapper(associationParametersData); throw new GLAccountInvalidClassificationException(accountClassification);
String sql = "select " + rm.schema(); }
// append SQL statement for fetching account totals
if (associationParametersData != null && associationParametersData.isRunningBalanceRequired()) { final GLAccountMapper rm = new GLAccountMapper(associationParametersData);
sql = sql + " and gl_j.id in (select t1.id from (select t2.account_id, max(t2.id) as id from " String sql = "select " + rm.schema();
+ "(select id, max(entry_date) as entry_date, account_id from acc_gl_journal_entry where is_running_balance_calculated = true " // append SQL statement for fetching account totals
+ "group by account_id desc, id) t3 inner join acc_gl_journal_entry t2 on t2.account_id = t3.account_id and t2.entry_date = t3.entry_date " if (associationParametersData != null && associationParametersData.isRunningBalanceRequired()) {
+ "group by t2.account_id desc) t1)"; sql = sql + " and gl_j.id in (select t1.id from (select t2.account_id, max(t2.id) as id from "
} + "(select id, max(entry_date) as entry_date, account_id from acc_gl_journal_entry where is_running_balance_calculated = true "
final Object[] parameterArray = new Object[3]; + "group by account_id desc, id) t3 inner join acc_gl_journal_entry t2 on t2.account_id = t3.account_id and t2.entry_date = t3.entry_date "
int arrayPos = 0; + "group by t2.account_id desc) t1)";
boolean filtersPresent = false; }
if ((accountClassification != null) || StringUtils.isNotBlank(searchParam) || (usage != null) || (manualTransactionsAllowed != null) final Object[] parameterArray = new Object[3];
|| (disabled != null)) { int arrayPos = 0;
filtersPresent = true; boolean filtersPresent = false;
sql += " where"; if ((accountClassification != null) || StringUtils.isNotBlank(searchParam) || (usage != null) || (manualTransactionsAllowed != null)
} || (disabled != null)) {
filtersPresent = true;
if (filtersPresent) { sql += " where";
boolean firstWhereConditionAdded = false; }
if (accountClassification != null) {
sql += " classification_enum = ?"; if (filtersPresent) {
parameterArray[arrayPos] = accountClassification.shortValue(); boolean firstWhereConditionAdded = false;
arrayPos = arrayPos + 1; if (accountClassification != null) {
firstWhereConditionAdded = true; sql += " classification_enum = ?";
} parameterArray[arrayPos] = accountClassification.shortValue();
if (StringUtils.isNotBlank(searchParam)) { arrayPos = arrayPos + 1;
if (firstWhereConditionAdded) { firstWhereConditionAdded = true;
sql += SQL_AND; }
} if (StringUtils.isNotBlank(searchParam)) {
sql += " ( name like %?% or gl_code like %?% )"; if (firstWhereConditionAdded) {
parameterArray[arrayPos] = searchParam; sql += SQL_AND;
arrayPos = arrayPos + 1; }
parameterArray[arrayPos] = searchParam; sql += " ( name like ? or gl_code like ? )";
arrayPos = arrayPos + 1; parameterArray[arrayPos] = "%" + searchParam + "%";
firstWhereConditionAdded = true; arrayPos = arrayPos + 1;
} parameterArray[arrayPos] = "%" + searchParam + "%";
if (usage != null) { arrayPos = arrayPos + 1;
if (firstWhereConditionAdded) { firstWhereConditionAdded = true;
sql += SQL_AND; }
} if (usage != null) {
if (GLAccountUsage.HEADER.getValue().equals(usage)) { if (firstWhereConditionAdded) {
sql += " account_usage = 2 "; sql += SQL_AND;
} else if (GLAccountUsage.DETAIL.getValue().equals(usage)) { }
sql += " account_usage = 1 "; if (GLAccountUsage.HEADER.getValue().equals(usage)) {
} sql += " account_usage = 2 ";
firstWhereConditionAdded = true; } else if (GLAccountUsage.DETAIL.getValue().equals(usage)) {
} sql += " account_usage = 1 ";
if (manualTransactionsAllowed != null) { }
if (firstWhereConditionAdded) { firstWhereConditionAdded = true;
sql += SQL_AND; }
} if (manualTransactionsAllowed != null) {
if (firstWhereConditionAdded) {
sql += " manual_journal_entries_allowed = " + manualTransactionsAllowed; sql += SQL_AND;
firstWhereConditionAdded = true; }
}
if (disabled != null) { sql += " manual_journal_entries_allowed = " + manualTransactionsAllowed;
if (firstWhereConditionAdded) { firstWhereConditionAdded = true;
sql += SQL_AND; }
} if (disabled != null) {
if (firstWhereConditionAdded) {
sql += " disabled = " + disabled; sql += SQL_AND;
} }
}
sql += " disabled = " + disabled;
sql += " ORDER BY gl_code ASC"; }
}
final Object[] finalObjectArray = Arrays.copyOf(parameterArray, arrayPos);
return this.jdbcTemplate.query(sql, rm, (Object[]) finalObjectArray);// NOSONAR sql += " ORDER BY gl_code ASC";
}
final Object[] finalObjectArray = Arrays.copyOf(parameterArray, arrayPos);
@Override return this.jdbcTemplate.query(sql, rm, (Object[]) finalObjectArray);// NOSONAR
public GLAccountData retrieveGLAccountById(final long glAccountId, JournalEntryAssociationParametersData associationParametersData) { }
try {
final GLAccountMapper rm = new GLAccountMapper(associationParametersData); @Override
final StringBuilder sql = new StringBuilder(); public GLAccountData retrieveGLAccountById(final long glAccountId, JournalEntryAssociationParametersData associationParametersData) {
sql.append("select ").append(rm.schema()); try {
sql.append(" where gl.id = ?"); final GLAccountMapper rm = new GLAccountMapper(associationParametersData);
if (associationParametersData.isRunningBalanceRequired()) { final StringBuilder sql = new StringBuilder();
sql.append(" and gl_j.is_running_balance_calculated = true ") sql.append("select ").append(rm.schema());
.append(" ORDER BY gl_j.entry_date DESC,gl_j.id DESC LIMIT 1"); sql.append(" where gl.id = ?");
}
final GLAccountData glAccount = this.jdbcTemplate.queryForObject(sql.toString(), rm, new Object[] { glAccountId });
return this.jdbcTemplate.queryForObject(sql.toString(), rm, new Object[] { glAccountId });
} catch (final EmptyResultDataAccessException e) { // Live balance: sum all non-reversed journal entries so cashier sees real-time balance
throw new GLAccountNotFoundException(glAccountId, e); // 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 "
@Override + "WHERE account_id = ? AND (reversed = false OR reversed IS NULL)";
public List<GLAccountData> retrieveAllEnabledDetailGLAccounts(final GLAccountType accountType) { final java.math.BigDecimal liveBalance = this.jdbcTemplate.queryForObject(liveBalanceSql,
return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.DETAIL.getValue(), null, false, java.math.BigDecimal.class, glAccountId);
new JournalEntryAssociationParametersData()); glAccount.setOrganizationRunningBalance(liveBalance != null ? liveBalance.longValue() : 0L);
} }
@Override return glAccount;
public List<GLAccountData> retrieveAllEnabledDetailGLAccounts() { } catch (final EmptyResultDataAccessException e) {
return retrieveAllGLAccounts(null, null, GLAccountUsage.DETAIL.getValue(), null, false, throw new GLAccountNotFoundException(glAccountId, e);
new JournalEntryAssociationParametersData()); }
} }
private static boolean checkValidGLAccountType(final int type) { @Override
for (final GLAccountType accountType : GLAccountType.values()) { public List<GLAccountData> retrieveAllEnabledDetailGLAccounts(final GLAccountType accountType) {
if (accountType.getValue().equals(type)) { return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.DETAIL.getValue(), null, false,
return true; new JournalEntryAssociationParametersData());
} }
}
return false; @Override
} public List<GLAccountData> retrieveAllEnabledDetailGLAccounts() {
return retrieveAllGLAccounts(null, null, GLAccountUsage.DETAIL.getValue(), null, false,
private static boolean checkValidGLAccountUsage(final int type) { new JournalEntryAssociationParametersData());
for (final GLAccountUsage accountUsage : GLAccountUsage.values()) { }
if (accountUsage.getValue().equals(type)) {
return true; private static boolean checkValidGLAccountType(final int type) {
} for (final GLAccountType accountType : GLAccountType.values()) {
} if (accountType.getValue().equals(type)) {
return false; return true;
} }
}
@Override return false;
public GLAccountData retrieveNewGLAccountDetails(final Integer type) { }
return GLAccountData.sensibleDefaultsForNewGLAccountCreation(type);
} private static boolean checkValidGLAccountUsage(final int type) {
for (final GLAccountUsage accountUsage : GLAccountUsage.values()) {
@Override if (accountUsage.getValue().equals(type)) {
public List<GLAccountData> retrieveAllEnabledHeaderGLAccounts(final GLAccountType accountType) { return true;
return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.HEADER.getValue(), null, false, }
new JournalEntryAssociationParametersData()); }
} return false;
}
@Override
public List<GLAccountDataForLookup> retrieveAccountsByTagId(final Long ruleId, final Integer transactionType) { @Override
final GLAccountDataLookUpMapper mapper = new GLAccountDataLookUpMapper(); public GLAccountData retrieveNewGLAccountDetails(final Integer type) {
final String sql = "Select " + GLAccountDataLookUpMapper.LOOKUP_SCHEMA + " where rule.id=? and tags.acc_type_enum=?"; return GLAccountData.sensibleDefaultsForNewGLAccountCreation(type);
return this.jdbcTemplate.query(sql, mapper, (Object[]) new Object[] { ruleId, transactionType });// NOSONAR }
}
@Override
private static final class GLAccountDataLookUpMapper implements RowMapper<GLAccountDataForLookup> { public List<GLAccountData> retrieveAllEnabledHeaderGLAccounts(final GLAccountType accountType) {
return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.HEADER.getValue(), null, false,
private static final String LOOKUP_SCHEMA = " gl.id as id, gl.name as name, gl.gl_code as glCode from acc_accounting_rule rule join acc_rule_tags tags on tags.acc_rule_id = rule.id join acc_gl_account gl on gl.tag_id=tags.tag_id"; new JournalEntryAssociationParametersData());
}
@Override
public GLAccountDataForLookup mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { @Override
final Long id = JdbcSupport.getLong(rs, "id"); public List<GLAccountDataForLookup> retrieveAccountsByTagId(final Long ruleId, final Integer transactionType) {
final String name = rs.getString("name"); final GLAccountDataLookUpMapper mapper = new GLAccountDataLookUpMapper();
final String glCode = rs.getString("glCode"); final String sql = "Select " + GLAccountDataLookUpMapper.LOOKUP_SCHEMA + " where rule.id=? and tags.acc_type_enum=?";
return this.jdbcTemplate.query(sql, mapper, (Object[]) new Object[] { ruleId, transactionType });// NOSONAR
return new GLAccountDataForLookup().setId(id).setName(name).setGlCode(glCode); }
}
} private static final class GLAccountDataLookUpMapper implements RowMapper<GLAccountDataForLookup> {
}
private static final String LOOKUP_SCHEMA = " gl.id as id, gl.name as name, gl.gl_code as glCode from acc_accounting_rule rule join acc_rule_tags tags on tags.acc_rule_id = rule.id join acc_gl_account gl on gl.tag_id=tags.tag_id";
@Override
public GLAccountDataForLookup mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = JdbcSupport.getLong(rs, "id");
final String name = rs.getString("name");
final String glCode = rs.getString("glCode");
return new GLAccountDataForLookup().setId(id).setName(name).setGlCode(glCode);
}
}
}
@@ -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,
@@ -54,6 +54,7 @@
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class> <class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
<class>org.apache.fineract.portfolio.group.domain.Group</class> <class>org.apache.fineract.portfolio.group.domain.Group</class>
<class>org.apache.fineract.portfolio.client.domain.Client</class> <class>org.apache.fineract.portfolio.client.domain.Client</class>
<class>org.apache.fineract.portfolio.client.domain.ClientKyc</class>
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class> <class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class>
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class> <class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class> <class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
@@ -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);
}
}
@@ -54,6 +54,7 @@
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class> <class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
<class>org.apache.fineract.portfolio.group.domain.Group</class> <class>org.apache.fineract.portfolio.group.domain.Group</class>
<class>org.apache.fineract.portfolio.client.domain.Client</class> <class>org.apache.fineract.portfolio.client.domain.Client</class>
<class>org.apache.fineract.portfolio.client.domain.ClientKyc</class>
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class> <class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class>
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class> <class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class> <class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
@@ -54,6 +54,7 @@
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class> <class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
<class>org.apache.fineract.portfolio.group.domain.Group</class> <class>org.apache.fineract.portfolio.group.domain.Group</class>
<class>org.apache.fineract.portfolio.client.domain.Client</class> <class>org.apache.fineract.portfolio.client.domain.Client</class>
<class>org.apache.fineract.portfolio.client.domain.ClientKyc</class>
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class> <class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class>
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class> <class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class> <class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
+1 -1
View File
@@ -183,7 +183,7 @@ task cleanupGeneratedJavaFiles() {
// Configure Java compilation // Configure Java compilation
tasks.named('compileJava') { tasks.named('compileJava') {
outputs.cacheIf { true } outputs.cacheIf { true }
dependsOn(buildJavaSdk, buildTypescriptAngularSdk, buildAsciidoc, cleanupGeneratedJavaFiles, licenseFormatMain, spotlessMiscApply) dependsOn(buildJavaSdk, cleanupGeneratedJavaFiles, licenseFormatMain, spotlessMiscApply)
mustRunAfter(licenseFormatMain, cleanupGeneratedJavaFiles) mustRunAfter(licenseFormatMain, cleanupGeneratedJavaFiles)
} }
@@ -53,6 +53,7 @@
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class> <class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
<class>org.apache.fineract.portfolio.group.domain.Group</class> <class>org.apache.fineract.portfolio.group.domain.Group</class>
<class>org.apache.fineract.portfolio.client.domain.Client</class> <class>org.apache.fineract.portfolio.client.domain.Client</class>
<class>org.apache.fineract.portfolio.client.domain.ClientKyc</class>
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class> <class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class>
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class> <class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class> <class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
@@ -436,7 +436,9 @@ public final class AccountingConstants {
CASH_AT_TELLER(102, "cashAtTeller", GLAccountType.ASSET), // CASH_AT_TELLER(102, "cashAtTeller", GLAccountType.ASSET), //
OPENING_BALANCES_TRANSFER_CONTRA(300, "openingBalancesTransferContra", GLAccountType.EQUITY), // OPENING_BALANCES_TRANSFER_CONTRA(300, "openingBalancesTransferContra", GLAccountType.EQUITY), //
ASSET_FUND_SOURCE(103, "fundSource", GLAccountType.ASSET), // ASSET_FUND_SOURCE(103, "fundSource", GLAccountType.ASSET), //
PAYABLE_DIVIDENDS(201, "payableDividends", GLAccountType.LIABILITY); // PAYABLE_DIVIDENDS(201, "payableDividends", GLAccountType.LIABILITY), //
RESERVE_FOR_DIVIDEND(301, "reserveForDividend", GLAccountType.EQUITY), //
DIVIDEND_FORFEITED_INCOME(400, "dividendForfeitedIncome", GLAccountType.INCOME); //
private final Integer value; private final Integer value;
private final String code; private final String code;
@@ -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;
}
} }
@@ -137,7 +137,7 @@ public class CommandSourceService {
String permission = commandSource.getPermissionCode(); String permission = commandSource.getPermissionCode();
boolean isMakerChecker = configurationDomainService.isMakerCheckerEnabledForTask(permission); boolean isMakerChecker = configurationDomainService.isMakerCheckerEnabledForTask(permission);
if (isMakerChecker || result.isRollbackTransaction()) { if (isMakerChecker || result.isRollbackTransaction()) {
if (isApprovedByChecker || user.isCheckerSuperUser()) { if (isApprovedByChecker || isSuperUserBypassAllowed(commandSource, user)) {
commandSource.markAsChecked(user); commandSource.markAsChecked(user);
} else { } else {
if (commandSource.isSanitized()) { if (commandSource.isSanitized()) {
@@ -151,6 +151,25 @@ public class CommandSourceService {
return result; return result;
} }
private boolean isSuperUserBypassAllowed(CommandSource commandSource, AppUser user) {
if (!user.isCheckerSuperUser()) {
return false;
}
if ("GLACCOUNT".equalsIgnoreCase(commandSource.getEntityName())
|| "CBSBASEINTERESTRATE".equalsIgnoreCase(commandSource.getEntityName())
|| "CHARGE".equalsIgnoreCase(commandSource.getEntityName())
|| "LOANPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|| "SAVINGSPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|| "CURRENTPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|| "SHAREPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|| "FIXEDDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|| "USER".equalsIgnoreCase(commandSource.getEntityName())
|| "ROLE".equalsIgnoreCase(commandSource.getEntityName())) {
return false;
}
return true;
}
private void sanitizeJson(@NonNull CommandSource commandSource, Set<String> sanitizeKeys) { private void sanitizeJson(@NonNull CommandSource commandSource, Set<String> sanitizeKeys) {
if (sanitizeKeys == null || sanitizeKeys.isEmpty()) { if (sanitizeKeys == null || sanitizeKeys.isEmpty()) {
return; return;
@@ -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";
@@ -1857,6 +2211,14 @@ public class CommandWrapperBuilder {
return this; return this;
} }
public CommandWrapperBuilder createCbsBaseInterestRate() {
this.actionName = "CREATE";
this.entityName = "CBSBASEINTERESTRATE";
this.entityId = null;
this.href = "/fixeddepositproducts/baseinterestrates";
return this;
}
public CommandWrapperBuilder updateFixedDepositProduct(final Long productId) { public CommandWrapperBuilder updateFixedDepositProduct(final Long productId) {
this.actionName = "UPDATE"; this.actionName = "UPDATE";
this.entityName = "FIXEDDEPOSITPRODUCT"; this.entityName = "FIXEDDEPOSITPRODUCT";
@@ -3565,7 +3927,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);
@@ -26,6 +26,8 @@ public interface ConfigurationDomainService {
boolean isMakerCheckerEnabledForTask(String taskPermissionCode); boolean isMakerCheckerEnabledForTask(String taskPermissionCode);
boolean isMakerCheckerEnabled();
/** /**
* Global kill-switch for MFA-factor-backed login enforcement. When true, users with at least * Global kill-switch for MFA-factor-backed login enforcement. When true, users with at least
* one verified, non-disabled row in {@code m_user_mfa_factor} must complete a 2FA challenge * one verified, non-disabled row in {@code m_user_mfa_factor} must complete a 2FA challenge
@@ -1,116 +1,150 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.infrastructure.core.serialization; 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 java.time.LocalDate; import com.google.gson.TypeAdapter;
import java.time.LocalDateTime; import com.google.gson.TypeAdapterFactory;
import java.time.LocalTime; import com.google.gson.reflect.TypeToken;
import java.time.MonthDay; import com.google.gson.stream.JsonReader;
import java.time.OffsetDateTime; import com.google.gson.stream.JsonWriter;
import java.time.ZonedDateTime; import java.io.IOException;
import java.util.ArrayList; import java.time.LocalDate;
import java.util.HashSet; import java.time.LocalDateTime;
import java.util.Set; import java.time.LocalTime;
import org.apache.fineract.infrastructure.core.api.DateAdapter; import java.time.MonthDay;
import org.apache.fineract.infrastructure.core.api.ExternalIdAdapter; import java.time.OffsetDateTime;
import org.apache.fineract.infrastructure.core.api.JodaDateTimeAdapter; import java.time.ZonedDateTime;
import org.apache.fineract.infrastructure.core.api.JodaMonthDayAdapter; import java.util.ArrayList;
import org.apache.fineract.infrastructure.core.api.LocalDateAdapter; import java.util.HashSet;
import org.apache.fineract.infrastructure.core.api.LocalDateTimeAdapter; import java.util.Set;
import org.apache.fineract.infrastructure.core.api.LocalTimeAdapter; import org.apache.fineract.infrastructure.core.api.DateAdapter;
import org.apache.fineract.infrastructure.core.api.OffsetDateTimeAdapter; import org.apache.fineract.infrastructure.core.api.ExternalIdAdapter;
import org.apache.fineract.infrastructure.core.api.ParameterListExclusionStrategy; import org.apache.fineract.infrastructure.core.api.JodaDateTimeAdapter;
import org.apache.fineract.infrastructure.core.api.ParameterListInclusionStrategy; import org.apache.fineract.infrastructure.core.api.JodaMonthDayAdapter;
import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.api.LocalDateAdapter;
import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException; import org.apache.fineract.infrastructure.core.api.LocalDateTimeAdapter;
import org.springframework.stereotype.Service; import org.apache.fineract.infrastructure.core.api.LocalTimeAdapter;
import org.apache.fineract.infrastructure.core.api.OffsetDateTimeAdapter;
/** import org.apache.fineract.infrastructure.core.api.ParameterListExclusionStrategy;
* Helper class for serialization of Java objects into JSON using Google's GSON. import org.apache.fineract.infrastructure.core.api.ParameterListInclusionStrategy;
*/ import org.apache.fineract.infrastructure.core.domain.ExternalId;
@Service import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException;
public final class GoogleGsonSerializerHelper { import org.springframework.stereotype.Service;
public Gson createGsonBuilderForPartialResponseFiltering(final Set<String> responseParameters) { /**
final ExclusionStrategy strategy = new ParameterListInclusionStrategy(responseParameters); * Helper class for serialization of Java objects into JSON using Google's GSON.
*/
final GsonBuilder builder = new GsonBuilder().addSerializationExclusionStrategy(strategy); @Service
registerTypeAdapters(builder); public final class GoogleGsonSerializerHelper {
return builder.create();
} public Gson createGsonBuilderForPartialResponseFiltering(final Set<String> responseParameters) {
final ExclusionStrategy strategy = new ParameterListInclusionStrategy(responseParameters);
public Gson createGsonBuilderWithParameterExclusionSerializationStrategy(final Set<String> supportedParameters,
final Set<String> responseParameters) { final GsonBuilder builder = new GsonBuilder().addSerializationExclusionStrategy(strategy);
registerTypeAdapters(builder);
final Set<String> parameterNamesToSkip = new HashSet<>(); return builder.create();
}
if (!responseParameters.isEmpty()) {
// strip out all known support parameters from expected response to public Gson createGsonBuilderWithParameterExclusionSerializationStrategy(final Set<String> supportedParameters,
// see if unsupported parameters requested for response. final Set<String> responseParameters) {
final Set<String> differentParametersDetectedSet = new HashSet<>(responseParameters);
differentParametersDetectedSet.removeAll(supportedParameters); final Set<String> parameterNamesToSkip = new HashSet<>();
if (!differentParametersDetectedSet.isEmpty()) { if (!responseParameters.isEmpty()) {
throw new UnsupportedParameterException(new ArrayList<>(differentParametersDetectedSet)); // strip out all known support parameters from expected response to
} // see if unsupported parameters requested for response.
final Set<String> differentParametersDetectedSet = new HashSet<>(responseParameters);
parameterNamesToSkip.addAll(supportedParameters); differentParametersDetectedSet.removeAll(supportedParameters);
parameterNamesToSkip.removeAll(responseParameters);
} if (!differentParametersDetectedSet.isEmpty()) {
throw new UnsupportedParameterException(new ArrayList<>(differentParametersDetectedSet));
final ExclusionStrategy strategy = new ParameterListExclusionStrategy(parameterNamesToSkip); }
final GsonBuilder builder = new GsonBuilder().addSerializationExclusionStrategy(strategy); parameterNamesToSkip.addAll(supportedParameters);
registerTypeAdapters(builder); parameterNamesToSkip.removeAll(responseParameters);
return builder.create(); }
}
final ExclusionStrategy strategy = new ParameterListExclusionStrategy(parameterNamesToSkip);
public String serializedJsonFrom(final Gson serializer, final Object[] dataObjects) {
return serializer.toJson(dataObjects); final GsonBuilder builder = new GsonBuilder().addSerializationExclusionStrategy(strategy);
} registerTypeAdapters(builder);
return builder.create();
public String serializedJsonFrom(final Gson serializer, final Object singleDataObject) { }
return serializer.toJson(singleDataObject);
} public String serializedJsonFrom(final Gson serializer, final Object[] dataObjects) {
return serializer.toJson(dataObjects);
public static Gson createSimpleGson() { }
return createGsonBuilder().create();
} public String serializedJsonFrom(final Gson serializer, final Object singleDataObject) {
return serializer.toJson(singleDataObject);
public static GsonBuilder createGsonBuilder() { }
final GsonBuilder builder = new GsonBuilder();
registerTypeAdapters(builder); public static Gson createSimpleGson() {
return builder; return createGsonBuilder().create();
} }
public static void registerTypeAdapters(final GsonBuilder builder) { public static GsonBuilder createGsonBuilder() {
builder.registerTypeAdapter(java.util.Date.class, new DateAdapter()); final GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(LocalDate.class, new LocalDateAdapter()); registerTypeAdapters(builder);
// NOTE: was missing, necessary for GSON serialization with JDK 21's restrictive module access return builder;
builder.registerTypeAdapter(LocalTime.class, new LocalTimeAdapter()); }
builder.registerTypeAdapter(ZonedDateTime.class, new JodaDateTimeAdapter());
builder.registerTypeAdapter(MonthDay.class, new JodaMonthDayAdapter()); public static void registerTypeAdapters(final GsonBuilder builder) {
builder.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter()); builder.registerTypeAdapter(java.util.Date.class, new DateAdapter());
builder.registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter()); builder.registerTypeAdapter(LocalDate.class, new LocalDateAdapter());
builder.registerTypeAdapter(ExternalId.class, new ExternalIdAdapter()); // NOTE: was missing, necessary for GSON serialization with JDK 21's restrictive module access
} builder.registerTypeAdapter(LocalTime.class, new LocalTimeAdapter());
} builder.registerTypeAdapter(ZonedDateTime.class, new JodaDateTimeAdapter());
builder.registerTypeAdapter(MonthDay.class, new JodaMonthDayAdapter());
builder.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter());
builder.registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter());
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;
}
}
@@ -38,6 +38,7 @@ public enum JobName {
GENERATE_RD_SCEHDULE("Generate Mandatory Savings Schedule"), // GENERATE_RD_SCEHDULE("Generate Mandatory Savings Schedule"), //
GENERATE_LOANLOSS_PROVISIONING("Generate Loan Loss Provisioning"), // GENERATE_LOANLOSS_PROVISIONING("Generate Loan Loss Provisioning"), //
POST_DIVIDENTS_FOR_SHARES("Post Dividends For Shares"), // POST_DIVIDENTS_FOR_SHARES("Post Dividends For Shares"), //
FORFEIT_UNCLAIMED_DIVIDENDS("Forfeit Unclaimed Dividends"), //
UPDATE_SAVINGS_DORMANT_ACCOUNTS("Update Savings Dormant Accounts"), // UPDATE_SAVINGS_DORMANT_ACCOUNTS("Update Savings Dormant Accounts"), //
ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_LOANS_WITH_INCOME_POSTED_AS_TRANSACTIONS( ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_LOANS_WITH_INCOME_POSTED_AS_TRANSACTIONS(
"Add Accrual Transactions For Loans With Income Posted As Transactions"), // "Add Accrual Transactions For Loans With Income Posted As Transactions"), //
@@ -61,6 +62,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;
@@ -1,71 +1,79 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account; package org.apache.fineract.portfolio.account;
public enum PortfolioAccountType { 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 String code; private final Integer value;
private final String code;
PortfolioAccountType(final Integer value, final String code) {
this.value = value; PortfolioAccountType(final Integer value, final String code) {
this.code = code; this.value = value;
} this.code = code;
}
public Integer getValue() {
return this.value; public Integer getValue() {
} return this.value;
}
public String getCode() {
return this.code; public String getCode() {
} return this.code;
}
public static PortfolioAccountType fromInt(final Integer type) {
public static PortfolioAccountType fromInt(final Integer type) {
PortfolioAccountType enumType = INVALID;
if (type != null) { PortfolioAccountType enumType = INVALID;
switch (type) { if (type != null) {
case 1: switch (type) {
enumType = LOAN; case 1:
break; enumType = LOAN;
case 2: break;
enumType = SAVINGS; case 2:
break; enumType = SAVINGS;
default: break;
enumType = INVALID; case 3:
} enumType = CURRENT;
} break;
return enumType; default:
} enumType = INVALID;
}
// TODO: bad practice and unnecessary code! why not just use the enum values themselves!?! }
public boolean isSavingsAccount() { return enumType;
return this.equals(SAVINGS); }
}
// TODO: bad practice and unnecessary code! why not just use the enum values themselves!?!
// TODO: bad practice and unnecessary code! why not just use the enum values themselves!?! public boolean isSavingsAccount() {
public boolean isLoanAccount() { return this.equals(SAVINGS);
return this.equals(LOAN); }
}
} // TODO: bad practice and unnecessary code! why not just use the enum values themselves!?!
public boolean isLoanAccount() {
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,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.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 for one customer inside the recurring-behaviour observation window. */
long countByClient_IdAndRaisedOnGreaterThanEqual(Long clientId, LocalDateTime since);
/**
* 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,109 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* 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;
/**
* A reviewer's judgement on a customer who repeatedly trips the AML engine.
*
* Distinct from the per-transaction disposition held on {@link AmlAlert}: that answers "was this
* transaction acceptable", this answers "is this customer's pattern of behaviour acceptable".
* {@link #alertCountAtReview} is what lets the queue clear — see the 0460 changelog.
*/
@Entity
@Table(name = "m_aml_recurring_review")
public class AmlRecurringReview {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(optional = false)
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@Column(name = "alert_count_at_review", nullable = false)
private Integer alertCountAtReview;
@Column(name = "outcome", nullable = false, length = 30)
private String outcome;
@Column(name = "notes", nullable = false, length = 1000)
private String notes;
@ManyToOne(optional = false)
@JoinColumn(name = "reviewed_by", nullable = false)
private AppUser reviewedBy;
@Column(name = "reviewed_on", nullable = false)
private LocalDateTime reviewedOn;
protected AmlRecurringReview() {}
public static AmlRecurringReview record(final Client client, final int alertCountAtReview,
final AmlRecurringReviewOutcome outcome, final String notes, final AppUser reviewedBy) {
final AmlRecurringReview review = new AmlRecurringReview();
review.client = client;
review.alertCountAtReview = alertCountAtReview;
review.outcome = outcome.name();
review.notes = notes;
review.reviewedBy = reviewedBy;
review.reviewedOn = LocalDateTime.now();
return review;
}
public Long getId() {
return id;
}
public Client getClient() {
return client;
}
public Integer getAlertCountAtReview() {
return alertCountAtReview;
}
public AmlRecurringReviewOutcome getOutcome() {
return AmlRecurringReviewOutcome.valueOf(outcome);
}
public String getNotes() {
return notes;
}
public AppUser getReviewedBy() {
return reviewedBy;
}
public LocalDateTime getReviewedOn() {
return reviewedOn;
}
}
@@ -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.aml.domain;
/** What a reviewer concluded about a customer's repeated AML activity. */
public enum AmlRecurringReviewOutcome {
/** Behaviour explained and acceptable — nothing further, but the customer stays visible if it recurs. */
CLEARED,
/** Not conclusive; keep watching this customer more closely. */
MONITOR,
/** Warrants an STR case; the reviewer escalates the individual alerts separately. */
ESCALATED
}
@@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AmlRecurringReviewRepository extends JpaRepository<AmlRecurringReview, Long> {
/** Newest first, so the caller can take the head as the latest review for a customer. */
List<AmlRecurringReview> findByClient_IdOrderByReviewedOnDesc(Long clientId);
}
@@ -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,163 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.aml.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.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() {}
/**
* Builds the live-transaction counterpart of a {@code ClientRiskRule} authored from the AML
* Rules Master screen. {@code sourceClientRiskRuleId} is the link the read side joins on, so
* the two rows are always created together and the rule code is derived from it.
*/
public static AmlTransactionRule createForRiskRule(final Long sourceClientRiskRuleId, final String description,
final AmlAppliesTo appliesTo, final AmlRuleType ruleType, final Integer windowDays,
final BigDecimal thresholdAmount, final BigDecimal minLimitAmount, final BigDecimal maxLimitAmount) {
final AmlTransactionRule rule = new AmlTransactionRule();
rule.sourceClientRiskRuleId = sourceClientRiskRuleId;
// rule_code is UNIQUE; the source rule id is unique per row, so this cannot collide.
rule.ruleCode = "CRR-" + sourceClientRiskRuleId;
rule.description = description;
rule.appliesTo = appliesTo.name();
rule.ruleType = ruleType.name();
rule.windowDays = windowDays;
rule.thresholdAmount = thresholdAmount;
rule.minLimitAmount = minLimitAmount;
rule.maxLimitAmount = maxLimitAmount;
rule.active = true;
return rule;
}
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";
@@ -175,6 +191,7 @@ public class ClientApiConstants {
public static final String constitutionIdParamName = "constitutionId"; public static final String constitutionIdParamName = "constitutionId";
public static final String mainBusinessLineIdParamName = "mainBusinessLineId"; public static final String mainBusinessLineIdParamName = "mainBusinessLineId";
public static final String authorizedSignatoryParamName = "authorizedSignatory"; public static final String authorizedSignatoryParamName = "authorizedSignatory";
public static final String authorizedSignatoryClientIdsParamName = "authorizedSignatoryClientIds";
public static final String additionalInfoParamName = "additionalInfo"; public static final String additionalInfoParamName = "additionalInfo";
public static final String dateOfRegistrationParamName = "dateOfRegistration"; public static final String dateOfRegistrationParamName = "dateOfRegistration";
@@ -248,8 +265,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;
@@ -99,6 +105,19 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
private Boolean alternateDipVerified; private Boolean alternateDipVerified;
private Long alternateDipLookupLogId; private Long alternateDipLookupLogId;
private String alternateDipTimestamp; private String alternateDipTimestamp;
/**
* Live DIP/MNRL status of the number, resolved from the latest lookup rather than the flag
* captured at onboarding. A customer cleared at onboarding whose number is later reported or
* reassigned would otherwise keep showing "Verified" forever.
*/
private String dipLiveStatus;
private String alternateDipLiveStatus;
/**
* Disposition of the client's most recent flagged sanctions alert, or null when they carry
* none. Like the DIP status this is resolved live: nameVerified is captured at onboarding and
* would keep reading "Clear" after the client was added to a later list snapshot.
*/
private String sanctionsAlertStatus;
private Boolean nameVerified; private Boolean nameVerified;
private Long screeningLogId; private Long screeningLogId;
private Boolean isStaff; private Boolean isStaff;
@@ -400,13 +419,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;
@@ -424,6 +449,11 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
data.alternateDipVerified = clientData.alternateDipVerified; data.alternateDipVerified = clientData.alternateDipVerified;
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId; data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
data.alternateDipTimestamp = clientData.alternateDipTimestamp; data.alternateDipTimestamp = clientData.alternateDipTimestamp;
// Carried across too: these helpers rebuild ClientData field by field, so a field missing
// here is silently dropped from the payload the client view loads.
data.dipLiveStatus = clientData.dipLiveStatus;
data.alternateDipLiveStatus = clientData.alternateDipLiveStatus;
data.sanctionsAlertStatus = clientData.sanctionsAlertStatus;
data.nameVerified = clientData.nameVerified; data.nameVerified = clientData.nameVerified;
data.screeningLogId = clientData.screeningLogId; data.screeningLogId = clientData.screeningLogId;
return data; return data;
@@ -456,13 +486,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;
@@ -480,6 +516,11 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
data.alternateDipVerified = clientData.alternateDipVerified; data.alternateDipVerified = clientData.alternateDipVerified;
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId; data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
data.alternateDipTimestamp = clientData.alternateDipTimestamp; data.alternateDipTimestamp = clientData.alternateDipTimestamp;
// Carried across too: these helpers rebuild ClientData field by field, so a field missing
// here is silently dropped from the payload the client view loads.
data.dipLiveStatus = clientData.dipLiveStatus;
data.alternateDipLiveStatus = clientData.alternateDipLiveStatus;
data.sanctionsAlertStatus = clientData.sanctionsAlertStatus;
data.nameVerified = clientData.nameVerified; data.nameVerified = clientData.nameVerified;
data.screeningLogId = clientData.screeningLogId; data.screeningLogId = clientData.screeningLogId;
return data; return data;
@@ -509,7 +550,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;
@@ -527,6 +573,11 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
data.alternateDipVerified = clientData.alternateDipVerified; data.alternateDipVerified = clientData.alternateDipVerified;
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId; data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
data.alternateDipTimestamp = clientData.alternateDipTimestamp; data.alternateDipTimestamp = clientData.alternateDipTimestamp;
// Carried across too: these helpers rebuild ClientData field by field, so a field missing
// here is silently dropped from the payload the client view loads.
data.dipLiveStatus = clientData.dipLiveStatus;
data.alternateDipLiveStatus = clientData.alternateDipLiveStatus;
data.sanctionsAlertStatus = clientData.sanctionsAlertStatus;
data.nameVerified = clientData.nameVerified; data.nameVerified = clientData.nameVerified;
data.screeningLogId = clientData.screeningLogId; data.screeningLogId = clientData.screeningLogId;
return data; return data;
@@ -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;
@@ -34,6 +34,8 @@ public class ClientNonPersonData implements Serializable {
private final CodeValueData mainBusinessLine; private final CodeValueData mainBusinessLine;
private final String remarks; private final String remarks;
private final String authorizedSignatory; private final String authorizedSignatory;
/** Client ids behind {@link #authorizedSignatory}, comma separated and in the same order. */
private final String authorizedSignatoryClientIds;
private final String additionalInfo; private final String additionalInfo;
private final LocalDate dateOfRegistration; private final LocalDate dateOfRegistration;
@@ -62,13 +64,24 @@ public class ClientNonPersonData implements Serializable {
this.constitution = null; this.constitution = null;
this.mainBusinessLine = null; this.mainBusinessLine = null;
this.authorizedSignatory = null; this.authorizedSignatory = null;
this.authorizedSignatoryClientIds = null;
this.additionalInfo = null; this.additionalInfo = null;
this.dateOfRegistration = null; this.dateOfRegistration = null;
} }
/**
* Kept for the list/search mappers, whose SQL does not select the signatory client ids.
*/
public ClientNonPersonData(CodeValueData constitution, String incorpNo, LocalDate incorpValidityTillDate, public ClientNonPersonData(CodeValueData constitution, String incorpNo, LocalDate incorpValidityTillDate,
CodeValueData mainBusinessLine, String remarks, String authorizedSignatory, String additionalInfo, CodeValueData mainBusinessLine, String remarks, String authorizedSignatory, String additionalInfo,
LocalDate dateOfRegistration) { LocalDate dateOfRegistration) {
this(constitution, incorpNo, incorpValidityTillDate, mainBusinessLine, remarks, authorizedSignatory, additionalInfo,
dateOfRegistration, null);
}
public ClientNonPersonData(CodeValueData constitution, String incorpNo, LocalDate incorpValidityTillDate,
CodeValueData mainBusinessLine, String remarks, String authorizedSignatory, String additionalInfo,
LocalDate dateOfRegistration, String authorizedSignatoryClientIds) {
this.constitution = constitution; this.constitution = constitution;
this.incorpNumber = incorpNo; this.incorpNumber = incorpNo;
@@ -76,6 +89,7 @@ public class ClientNonPersonData implements Serializable {
this.mainBusinessLine = mainBusinessLine; this.mainBusinessLine = mainBusinessLine;
this.remarks = remarks; this.remarks = remarks;
this.authorizedSignatory = authorizedSignatory; this.authorizedSignatory = authorizedSignatory;
this.authorizedSignatoryClientIds = authorizedSignatoryClientIds;
this.additionalInfo = additionalInfo; this.additionalInfo = additionalInfo;
this.dateOfRegistration = dateOfRegistration; this.dateOfRegistration = dateOfRegistration;
} }
@@ -27,6 +27,7 @@ import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany; import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint; import jakarta.persistence.UniqueConstraint;
import java.time.LocalDate; import java.time.LocalDate;
@@ -77,6 +78,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 +235,28 @@ 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;
@OneToOne(mappedBy = "client", fetch = FetchType.LAZY)
private ClientKyc clientKyc;
@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;
@@ -271,6 +294,17 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "alternate_dip_timestamp") @Column(name = "alternate_dip_timestamp")
private String alternateDipTimestamp; private String alternateDipTimestamp;
/**
* The DIP/MNRL status the number actually returned — CLEAN, SUSPECTED, DISCONNECTED or
* SERVICE_UNAVAILABLE. dipVerified alone cannot tell "checked and clean" apart from "the DoT
* service was down", which previously stored both as verified.
*/
@Column(name = "dip_status", length = 40)
private String dipStatus;
@Column(name = "alternate_dip_status", length = 40)
private String alternateDipStatus;
@Column(name = "name_verified") @Column(name = "name_verified")
private Boolean nameVerified; private Boolean nameVerified;
@@ -598,6 +632,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 +870,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;
} }
} }
@@ -25,13 +25,19 @@ import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.persistence.UniqueConstraint; import jakarta.persistence.UniqueConstraint;
import java.time.LocalDate; import java.time.LocalDate;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map; import java.util.Map;
import java.util.Set;
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
@@ -59,6 +65,14 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@JoinColumn(name = "client_id", nullable = false) @JoinColumn(name = "client_id", nullable = false)
private Client client; private Client client;
/**
* Scratch record of what the in-flight {@link #update} replaced, read back via
* {@link #getFieldsOverwrittenByLastUpdate()}. Never persisted and never read outside the
* transaction that called update(), which clears it first.
*/
@Transient
private final transient Set<String> fieldsOverwrittenByLastUpdate = new LinkedHashSet<>();
/** 100 = PENDING, 200 = IN_PROGRESS, 300 = VERIFIED, 400 = REJECTED, 500 = EXPIRED */ /** 100 = PENDING, 200 = IN_PROGRESS, 300 = VERIFIED, 400 = REJECTED, 500 = EXPIRED */
@Column(name = "kyc_status", nullable = false) @Column(name = "kyc_status", nullable = false)
private Integer kycStatus; private Integer kycStatus;
@@ -102,6 +116,36 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "pan_status", length = 20) @Column(name = "pan_status", length = 20)
private String panStatus; private String panStatus;
// ── PAN declaration ──────────────────────────────────────────────────────
// Accepted in place of a passing PAN check. The Income Tax check matches on name AND date of
// birth, so a customer whose PAN card carries a different spelling — or a different DOB — can
// never clear it however many times the officer retries. Rather than leave such a client
// permanently in KYC exception, the branch takes a signed declaration recording what differs.
// Holding that declaration is what suppresses the exception, so it is stored, not just shown.
/** True once a signed declaration has been accepted for a PAN that could not be verified. */
@Column(name = "pan_declaration_taken")
private Boolean panDeclarationTaken;
/** What the declaration covers: "NAME", "DOB", "NAME,DOB", or "NONE" when PAN simply failed. */
@Column(name = "pan_declaration_mismatch_fields", length = 100)
private String panDeclarationMismatchFields;
/** Name exactly as printed on the physical PAN card, keyed in by the officer. */
@Column(name = "pan_declaration_name_on_pan", length = 200)
private String panDeclarationNameOnPan;
/** Date of birth exactly as printed on the physical PAN card. */
@Column(name = "pan_declaration_dob_on_pan")
private LocalDate panDeclarationDobOnPan;
/** Officer's free-text note — why the mismatch exists and what proof was sighted. */
@Column(name = "pan_declaration_remarks", length = 500)
private String panDeclarationRemarks;
@Column(name = "pan_declaration_taken_on")
private LocalDate panDeclarationTakenOn;
// ── Aadhaar ────────────────────────────────────────────────────────────── // ── Aadhaar ──────────────────────────────────────────────────────────────
/** Stored masked (last 4 digits visible). Full number must never be persisted as per UIDAI directive. */ /** Stored masked (last 4 digits visible). Full number must never be persisted as per UIDAI directive. */
@Column(name = "aadhaar_number_masked", length = 12) @Column(name = "aadhaar_number_masked", length = 12)
@@ -113,6 +157,31 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "aadhaar_verified_on") @Column(name = "aadhaar_verified_on")
private LocalDate aadhaarVerifiedOn; private LocalDate aadhaarVerifiedOn;
// ── Aadhaar enrolment receipt ────────────────────────────────────────────
// The counterpart of the PAN declaration, for a customer who has applied for Aadhaar but does
// not hold one yet. UIDAI issues an enrolment slip carrying a 28-digit EID, and until Aadhaar
// itself is issued there is no number to put through the OTP flow — the check cannot pass
// however long the branch waits. Recording the receipt is what waives the exception.
/** True once the customer's Aadhaar enrolment slip has been sighted and recorded. */
@Column(name = "aadhaar_receipt_taken")
private Boolean aadhaarReceiptTaken;
/** 28-digit Enrolment ID from the slip, stored with its printed separators. */
@Column(name = "aadhaar_enrolment_id", length = 40)
private String aadhaarEnrolmentId;
/** Enrolment date printed on the slip — a receipt is temporary, so its age matters. */
@Column(name = "aadhaar_enrolment_date")
private LocalDate aadhaarEnrolmentDate;
/** Officer's note on what was sighted. */
@Column(name = "aadhaar_receipt_remarks", length = 500)
private String aadhaarReceiptRemarks;
@Column(name = "aadhaar_receipt_taken_on")
private LocalDate aadhaarReceiptTakenOn;
// ── GSTIN (mandatory for NON_INDIVIDUAL clients) ───────────────────────── // ── GSTIN (mandatory for NON_INDIVIDUAL clients) ─────────────────────────
@Column(name = "gstin_number", length = 15) @Column(name = "gstin_number", length = 15)
private String gstinNumber; private String gstinNumber;
@@ -194,6 +263,18 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "freeze_authority", length = 100) @Column(name = "freeze_authority", length = 100)
private String freezeAuthority; private String freezeAuthority;
@Column(name = "freeze_date")
private LocalDate freezeDate;
@Column(name = "freeze_amount", precision = 19, scale = 6)
private BigDecimal freezeAmount;
@Column(name = "unfreeze_date")
private LocalDate unfreezeDate;
@Column(name = "no_debit_date")
private LocalDate noDebitDate;
// ── Form 60 / Form 15G/15H ─────────────────────────────────────────────── // ── Form 60 / Form 15G/15H ───────────────────────────────────────────────
@Column(name = "form60_type", length = 20) @Column(name = "form60_type", length = 20)
private String form60Type; private String form60Type;
@@ -240,6 +321,18 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "remarks", length = 500) @Column(name = "remarks", length = 500)
private String remarks; private String remarks;
@Column(name = "is_exception", nullable = false)
private Boolean isException;
@Column(name = "exception_reason", length = 200)
private String exceptionReason;
@Column(name = "exception_resolved", nullable = false)
private Boolean exceptionResolved;
@Column(name = "exception_resolved_on")
private LocalDate exceptionResolvedOn;
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────
protected ClientKyc() {} protected ClientKyc() {}
@@ -260,7 +353,9 @@ 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.isException = false;
this.exceptionResolved = false;
this.initiatedOn = DateUtils.getLocalDateOfTenant();
} }
public static ClientKyc initiateKyc(final Client client, final JsonCommand command) { public static ClientKyc initiateKyc(final Client client, final JsonCommand command) {
@@ -287,6 +382,24 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
private void applyExtendedFields(final JsonCommand command) { private void applyExtendedFields(final JsonCommand command) {
this.panAadhaarSeedingStatus = StringUtils.defaultIfEmpty( this.panAadhaarSeedingStatus = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("panAadhaarSeedingStatus"), this.panAadhaarSeedingStatus); command.stringValueOfParameterNamed("panAadhaarSeedingStatus"), this.panAadhaarSeedingStatus);
final Boolean panDeclarationTakenVal = command.booleanObjectValueOfParameterNamed("panDeclarationTaken");
this.panDeclarationTaken = panDeclarationTakenVal != null ? panDeclarationTakenVal : false;
this.panDeclarationMismatchFields = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("panDeclarationMismatchFields"), null);
this.panDeclarationNameOnPan = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("panDeclarationNameOnPan"), null);
this.panDeclarationDobOnPan = command.localDateValueOfParameterNamed("panDeclarationDobOnPan");
this.panDeclarationRemarks = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("panDeclarationRemarks"), null);
this.panDeclarationTakenOn = command.localDateValueOfParameterNamed("panDeclarationTakenOn");
final Boolean aadhaarReceiptTakenVal = command.booleanObjectValueOfParameterNamed("aadhaarReceiptTaken");
this.aadhaarReceiptTaken = aadhaarReceiptTakenVal != null ? aadhaarReceiptTakenVal : false;
this.aadhaarEnrolmentId = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("aadhaarEnrolmentId"), null);
this.aadhaarEnrolmentDate = command.localDateValueOfParameterNamed("aadhaarEnrolmentDate");
this.aadhaarReceiptRemarks = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("aadhaarReceiptRemarks"), null);
this.aadhaarReceiptTakenOn = command.localDateValueOfParameterNamed("aadhaarReceiptTakenOn");
this.enterprisePan = StringUtils.defaultIfEmpty( this.enterprisePan = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("enterprisePan"), null); command.stringValueOfParameterNamed("enterprisePan"), null);
this.cinNumber = StringUtils.defaultIfEmpty( this.cinNumber = StringUtils.defaultIfEmpty(
@@ -316,6 +429,10 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
command.stringValueOfParameterNamed("freezeReason"), null); command.stringValueOfParameterNamed("freezeReason"), null);
this.freezeAuthority = StringUtils.defaultIfEmpty( this.freezeAuthority = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("freezeAuthority"), null); command.stringValueOfParameterNamed("freezeAuthority"), null);
this.freezeDate = command.localDateValueOfParameterNamed("freezeDate");
this.freezeAmount = command.bigDecimalValueOfParameterNamed("freezeAmount");
this.unfreezeDate = command.localDateValueOfParameterNamed("unfreezeDate");
this.noDebitDate = command.localDateValueOfParameterNamed("noDebitDate");
this.form60Type = StringUtils.defaultIfEmpty( this.form60Type = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("form60Type"), null); command.stringValueOfParameterNamed("form60Type"), null);
this.form60Date = command.localDateValueOfParameterNamed("form60Date"); this.form60Date = command.localDateValueOfParameterNamed("form60Date");
@@ -324,15 +441,24 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
command.stringValueOfParameterNamed("form15Type"), null); command.stringValueOfParameterNamed("form15Type"), null);
this.form15Fy = StringUtils.defaultIfEmpty( this.form15Fy = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("form15FY"), null); command.stringValueOfParameterNamed("form15FY"), null);
Boolean isExceptionVal = command.booleanObjectValueOfParameterNamed("isException");
this.isException = isExceptionVal != null ? isExceptionVal : false;
this.exceptionReason = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("exceptionReason"), null);
Boolean exceptionResolvedVal = command.booleanObjectValueOfParameterNamed("exceptionResolved");
this.exceptionResolved = exceptionResolvedVal != null ? exceptionResolvedVal : false;
this.exceptionResolvedOn = command.localDateValueOfParameterNamed("exceptionResolvedOn");
} }
public Map<String, Object> update(final JsonCommand command) { public Map<String, Object> update(final JsonCommand command) {
final Map<String, Object> actualChanges = new LinkedHashMap<>(); final Map<String, Object> actualChanges = new LinkedHashMap<>();
this.fieldsOverwrittenByLastUpdate.clear();
final String panNumberParam = "panNumber"; final String panNumberParam = "panNumber";
if (command.isChangeInStringParameterNamed(panNumberParam, this.panNumber)) { if (command.isChangeInStringParameterNamed(panNumberParam, this.panNumber)) {
final String newValue = command.stringValueOfParameterNamed(panNumberParam); final String newValue = command.stringValueOfParameterNamed(panNumberParam);
actualChanges.put(panNumberParam, newValue); actualChanges.put(panNumberParam, newValue);
recordOverwrite(panNumberParam, this.panNumber);
this.panNumber = StringUtils.defaultIfEmpty(newValue, null); this.panNumber = StringUtils.defaultIfEmpty(newValue, null);
} }
@@ -340,6 +466,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
if (command.isChangeInStringParameterNamed(aadhaarParam, this.aadhaarNumberMasked)) { if (command.isChangeInStringParameterNamed(aadhaarParam, this.aadhaarNumberMasked)) {
final String newValue = command.stringValueOfParameterNamed(aadhaarParam); final String newValue = command.stringValueOfParameterNamed(aadhaarParam);
actualChanges.put(aadhaarParam, newValue); actualChanges.put(aadhaarParam, newValue);
recordOverwrite(aadhaarParam, this.aadhaarNumberMasked);
this.aadhaarNumberMasked = StringUtils.defaultIfEmpty(newValue, null); this.aadhaarNumberMasked = StringUtils.defaultIfEmpty(newValue, null);
} }
@@ -347,6 +474,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
if (command.isChangeInStringParameterNamed(gstinParam, this.gstinNumber)) { if (command.isChangeInStringParameterNamed(gstinParam, this.gstinNumber)) {
final String newValue = command.stringValueOfParameterNamed(gstinParam); final String newValue = command.stringValueOfParameterNamed(gstinParam);
actualChanges.put(gstinParam, newValue); actualChanges.put(gstinParam, newValue);
recordOverwrite(gstinParam, this.gstinNumber);
this.gstinNumber = StringUtils.defaultIfEmpty(newValue, null); this.gstinNumber = StringUtils.defaultIfEmpty(newValue, null);
} }
@@ -354,9 +482,12 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
if (command.isChangeInIntegerParameterNamed(riskParam, this.riskCategory)) { if (command.isChangeInIntegerParameterNamed(riskParam, this.riskCategory)) {
final Integer newValue = command.integerValueOfParameterNamed(riskParam); final Integer newValue = command.integerValueOfParameterNamed(riskParam);
actualChanges.put(riskParam, newValue); actualChanges.put(riskParam, newValue);
recordOverwrite(riskParam, this.riskCategory);
this.riskCategory = newValue; this.riskCategory = newValue;
} }
// remarks is the audit justification itself, so a change to it is deliberately not recorded
// as an overwrite — requiring remarks in order to write remarks would be circular.
final String remarksParam = "remarks"; final String remarksParam = "remarks";
if (command.isChangeInStringParameterNamed(remarksParam, this.remarks)) { if (command.isChangeInStringParameterNamed(remarksParam, this.remarks)) {
final String newValue = command.stringValueOfParameterNamed(remarksParam); final String newValue = command.stringValueOfParameterNamed(remarksParam);
@@ -366,6 +497,28 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
updateStringField(command, "panAadhaarSeedingStatus", this.panAadhaarSeedingStatus, actualChanges, updateStringField(command, "panAadhaarSeedingStatus", this.panAadhaarSeedingStatus, actualChanges,
v -> this.panAadhaarSeedingStatus = v); v -> this.panAadhaarSeedingStatus = v);
updateBooleanField(command, "panDeclarationTaken", this.panDeclarationTaken, actualChanges,
v -> this.panDeclarationTaken = v != null ? v : false);
updateStringField(command, "panDeclarationMismatchFields", this.panDeclarationMismatchFields, actualChanges,
v -> this.panDeclarationMismatchFields = v);
updateStringField(command, "panDeclarationNameOnPan", this.panDeclarationNameOnPan, actualChanges,
v -> this.panDeclarationNameOnPan = v);
updateDateField(command, "panDeclarationDobOnPan", this.panDeclarationDobOnPan, actualChanges,
v -> this.panDeclarationDobOnPan = v);
updateStringField(command, "panDeclarationRemarks", this.panDeclarationRemarks, actualChanges,
v -> this.panDeclarationRemarks = v);
updateDateField(command, "panDeclarationTakenOn", this.panDeclarationTakenOn, actualChanges,
v -> this.panDeclarationTakenOn = v);
updateBooleanField(command, "aadhaarReceiptTaken", this.aadhaarReceiptTaken, actualChanges,
v -> this.aadhaarReceiptTaken = v != null ? v : false);
updateStringField(command, "aadhaarEnrolmentId", this.aadhaarEnrolmentId, actualChanges,
v -> this.aadhaarEnrolmentId = v);
updateDateField(command, "aadhaarEnrolmentDate", this.aadhaarEnrolmentDate, actualChanges,
v -> this.aadhaarEnrolmentDate = v);
updateStringField(command, "aadhaarReceiptRemarks", this.aadhaarReceiptRemarks, actualChanges,
v -> this.aadhaarReceiptRemarks = v);
updateDateField(command, "aadhaarReceiptTakenOn", this.aadhaarReceiptTakenOn, actualChanges,
v -> this.aadhaarReceiptTakenOn = v);
updateStringField(command, "enterprisePan", this.enterprisePan, actualChanges, v -> this.enterprisePan = v); updateStringField(command, "enterprisePan", this.enterprisePan, actualChanges, v -> this.enterprisePan = v);
updateStringField(command, "cinNumber", this.cinNumber, actualChanges, v -> this.cinNumber = v); updateStringField(command, "cinNumber", this.cinNumber, actualChanges, v -> this.cinNumber = v);
updateStringField(command, "dinNumber", this.dinNumber, actualChanges, v -> this.dinNumber = v); updateStringField(command, "dinNumber", this.dinNumber, actualChanges, v -> this.dinNumber = v);
@@ -391,12 +544,23 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
updateStringField(command, "freezeReason", this.freezeReason, actualChanges, v -> this.freezeReason = v); updateStringField(command, "freezeReason", this.freezeReason, actualChanges, v -> this.freezeReason = v);
updateStringField(command, "freezeAuthority", this.freezeAuthority, actualChanges, updateStringField(command, "freezeAuthority", this.freezeAuthority, actualChanges,
v -> this.freezeAuthority = v); v -> this.freezeAuthority = v);
updateDateField(command, "freezeDate", this.freezeDate, actualChanges, v -> this.freezeDate = v);
updateBigDecimalField(command, "freezeAmount", this.freezeAmount, actualChanges, v -> this.freezeAmount = v);
updateDateField(command, "unfreezeDate", this.unfreezeDate, actualChanges, v -> this.unfreezeDate = v);
updateDateField(command, "noDebitDate", this.noDebitDate, actualChanges, v -> this.noDebitDate = v);
updateStringField(command, "form60Type", this.form60Type, actualChanges, v -> this.form60Type = v); updateStringField(command, "form60Type", this.form60Type, actualChanges, v -> this.form60Type = v);
updateDateField(command, "form60Date", this.form60Date, actualChanges, v -> this.form60Date = v); updateDateField(command, "form60Date", this.form60Date, actualChanges, v -> this.form60Date = v);
updateBooleanField(command, "form15Applicable", this.form15Applicable, actualChanges, updateBooleanField(command, "form15Applicable", this.form15Applicable, actualChanges,
v -> this.form15Applicable = v); v -> this.form15Applicable = v);
updateStringField(command, "form15Type", this.form15Type, actualChanges, v -> this.form15Type = v); updateStringField(command, "form15Type", this.form15Type, actualChanges, v -> this.form15Type = v);
updateStringField(command, "form15FY", this.form15Fy, actualChanges, v -> this.form15Fy = v); updateStringField(command, "form15FY", this.form15Fy, actualChanges, v -> this.form15Fy = v);
updateBooleanField(command, "isException", this.isException, actualChanges, v -> this.isException = v != null ? v : false);
updateStringField(command, "exceptionReason", this.exceptionReason, actualChanges, v -> this.exceptionReason = v);
updateBooleanField(command, "exceptionResolved", this.exceptionResolved, actualChanges, v -> this.exceptionResolved = v != null ? v : false);
updateDateField(command, "exceptionResolvedOn", this.exceptionResolvedOn, actualChanges, v -> this.exceptionResolvedOn = v);
updateBooleanField(command, "panVerified", this.panVerified, actualChanges, v -> this.panVerified = v != null ? v : false);
updateBooleanField(command, "aadhaarVerified", this.aadhaarVerified, actualChanges, v -> this.aadhaarVerified = v != null ? v : false);
updateBooleanField(command, "gstinVerified", this.gstinVerified, actualChanges, v -> this.gstinVerified = v != null ? v : false);
return actualChanges; return actualChanges;
} }
@@ -406,6 +570,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
if (command.isChangeInStringParameterNamed(param, current)) { if (command.isChangeInStringParameterNamed(param, current)) {
final String newValue = command.stringValueOfParameterNamed(param); final String newValue = command.stringValueOfParameterNamed(param);
changes.put(param, newValue); changes.put(param, newValue);
recordOverwrite(param, current);
setter.accept(StringUtils.defaultIfEmpty(newValue, null)); setter.accept(StringUtils.defaultIfEmpty(newValue, null));
} }
} }
@@ -415,6 +580,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
if (command.isChangeInLocalDateParameterNamed(param, current)) { if (command.isChangeInLocalDateParameterNamed(param, current)) {
final LocalDate newValue = command.localDateValueOfParameterNamed(param); final LocalDate newValue = command.localDateValueOfParameterNamed(param);
changes.put(param, newValue); changes.put(param, newValue);
recordOverwrite(param, current);
setter.accept(newValue); setter.accept(newValue);
} }
} }
@@ -425,11 +591,51 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
final Boolean newValue = command.booleanObjectValueOfParameterNamed(param); final Boolean newValue = command.booleanObjectValueOfParameterNamed(param);
if ((newValue == null && current != null) || (newValue != null && !newValue.equals(current))) { if ((newValue == null && current != null) || (newValue != null && !newValue.equals(current))) {
changes.put(param, newValue); changes.put(param, newValue);
recordOverwrite(param, current);
setter.accept(newValue); setter.accept(newValue);
} }
} }
} }
private void updateBigDecimalField(final JsonCommand command, final String param, final BigDecimal current,
final Map<String, Object> changes, final java.util.function.Consumer<BigDecimal> setter) {
if (command.isChangeInBigDecimalParameterNamed(param, current)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(param);
changes.put(param, newValue);
recordOverwrite(param, current);
setter.accept(newValue);
}
}
/**
* Notes that {@code param} replaced a value the record already held. A field going from empty to
* populated is the record being completed rather than altered, so it is deliberately not
* recorded — see {@link #getFieldsOverwrittenByLastUpdate()}.
*/
private void recordOverwrite(final String param, final Object currentValue) {
final boolean wasPopulated = currentValue instanceof String text
? StringUtils.isNotBlank(text)
: currentValue != null;
if (wasPopulated) {
this.fieldsOverwrittenByLastUpdate.add(param);
}
}
/**
* Parameters whose already-populated value the most recent {@link #update} replaced.
*
* <p>Distinguishes altering a record from finishing one: a non-individual KYC auto-verifies as
* soon as PAN and GSTIN pass, which happens part-way through the create wizard, and the officer
* still has to save the KYC document details afterwards. Those fields are empty at that point,
* so filling them in leaves this set empty and no audit justification is demanded — while
* changing a document number that was already on the verified record does populate it.
*
* <p>Reset at the start of every {@link #update} and never persisted: it describes one call.
*/
public Set<String> getFieldsOverwrittenByLastUpdate() {
return Collections.unmodifiableSet(this.fieldsOverwrittenByLastUpdate);
}
/** Called when documents are submitted and 3rd-party verification is initiated. */ /** Called when documents are submitted and 3rd-party verification is initiated. */
public void markInProgress() { public void markInProgress() {
this.kycStatus = ClientKycStatus.IN_PROGRESS.getValue(); this.kycStatus = ClientKycStatus.IN_PROGRESS.getValue();
@@ -456,13 +662,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 +680,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 +713,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 +791,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;
} }
@@ -621,6 +836,60 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return panStatus; return panStatus;
} }
public Boolean getPanDeclarationTaken() {
return panDeclarationTaken;
}
/** Null-safe form for callers deciding whether the PAN exception is waived. */
public boolean isPanDeclarationTaken() {
return Boolean.TRUE.equals(this.panDeclarationTaken);
}
public String getPanDeclarationMismatchFields() {
return panDeclarationMismatchFields;
}
public String getPanDeclarationNameOnPan() {
return panDeclarationNameOnPan;
}
public LocalDate getPanDeclarationDobOnPan() {
return panDeclarationDobOnPan;
}
public String getPanDeclarationRemarks() {
return panDeclarationRemarks;
}
public LocalDate getPanDeclarationTakenOn() {
return panDeclarationTakenOn;
}
public Boolean getAadhaarReceiptTaken() {
return aadhaarReceiptTaken;
}
/** Null-safe form for callers deciding whether the Aadhaar exception is waived. */
public boolean isAadhaarReceiptTaken() {
return Boolean.TRUE.equals(this.aadhaarReceiptTaken);
}
public String getAadhaarEnrolmentId() {
return aadhaarEnrolmentId;
}
public LocalDate getAadhaarEnrolmentDate() {
return aadhaarEnrolmentDate;
}
public String getAadhaarReceiptRemarks() {
return aadhaarReceiptRemarks;
}
public LocalDate getAadhaarReceiptTakenOn() {
return aadhaarReceiptTakenOn;
}
public String getAadhaarNumberMasked() { public String getAadhaarNumberMasked() {
return aadhaarNumberMasked; return aadhaarNumberMasked;
} }
@@ -785,6 +1054,38 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return form15Fy; return form15Fy;
} }
public Boolean getIsException() {
return isException;
}
public void setIsException(Boolean isException) {
this.isException = isException;
}
public String getExceptionReason() {
return exceptionReason;
}
public void setExceptionReason(String exceptionReason) {
this.exceptionReason = exceptionReason;
}
public Boolean getExceptionResolved() {
return exceptionResolved;
}
public void setExceptionResolved(Boolean exceptionResolved) {
this.exceptionResolved = exceptionResolved;
}
public LocalDate getExceptionResolvedOn() {
return exceptionResolvedOn;
}
public void setExceptionResolvedOn(LocalDate exceptionResolvedOn) {
this.exceptionResolvedOn = exceptionResolvedOn;
}
@Column(name = "is_active", nullable = false) @Column(name = "is_active", nullable = false)
private Boolean isActive = Boolean.TRUE; private Boolean isActive = Boolean.TRUE;
@@ -836,4 +1137,20 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
public Long getVersion() { public Long getVersion() {
return this.version; return this.version;
} }
public LocalDate getFreezeDate() {
return this.freezeDate;
}
public BigDecimal getFreezeAmount() {
return this.freezeAmount;
}
public LocalDate getUnfreezeDate() {
return this.unfreezeDate;
}
public LocalDate getNoDebitDate() {
return this.noDebitDate;
}
} }
@@ -0,0 +1,204 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* 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.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
/**
* One customer-outreach attempt against an open KYC exception.
*
* <p>Append-only by design — there is no setter-driven update path and the API exposes no PUT or
* DELETE. A contact log that can be edited after the fact is worthless as evidence, and RBI's KYC
* Master Direction expects the bank to be able to show what it told the customer and when.
*
* <p>Destinations are stored masked ({@code destinationMasked}); the full mobile number / email
* lives on {@code m_client} and is resolved server-side at send time, never accepted from the
* browser.
*/
@Entity
@Table(name = "m_client_kyc_contact_log")
public class ClientKycContactLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@ManyToOne
@JoinColumn(name = "client_id", nullable = false)
private Client client;
/** The KYC record the exception belongs to. Nullable — outreach can precede a KYC row. */
@Column(name = "kyc_id")
private Long kycId;
@Enumerated(EnumType.STRING)
@Column(name = "channel", length = 20, nullable = false)
private KycContactChannel channel;
@Column(name = "notes", length = 1000, nullable = false)
private String notes;
@Column(name = "officer_name", length = 200)
private String officerName;
@Column(name = "logged_by_user_id")
private Long loggedByUserId;
@Column(name = "exception_reason", length = 200)
private String exceptionReason;
@Enumerated(EnumType.STRING)
@Column(name = "delivery_status", length = 30, nullable = false)
private KycContactDeliveryStatus deliveryStatus;
@Column(name = "destination_masked", length = 120)
private String destinationMasked;
@Column(name = "provider", length = 40)
private String provider;
@Column(name = "provider_reference", length = 120)
private String providerReference;
@Column(name = "failure_reason", length = 500)
private String failureReason;
@Column(name = "message_body", length = 2000)
private String messageBody;
@Column(name = "contacted_at", nullable = false)
private LocalDateTime contactedAt;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
protected ClientKycContactLog() {
// JPA
}
public ClientKycContactLog(final Client client, final Long kycId, final KycContactChannel channel, final String notes,
final String officerName, final Long loggedByUserId, final String exceptionReason, final LocalDateTime contactedAt) {
this.client = client;
this.kycId = kycId;
this.channel = channel;
this.notes = notes;
this.officerName = officerName;
this.loggedByUserId = loggedByUserId;
this.exceptionReason = exceptionReason;
this.contactedAt = contactedAt;
this.createdAt = contactedAt;
this.deliveryStatus = KycContactDeliveryStatus.NOT_APPLICABLE;
}
/**
* Stamp the outcome of a platform send onto this log row. Called once, before the row is
* persisted — the entity is append-only afterwards.
*/
public void recordDeliveryOutcome(final KycContactDeliveryStatus status, final String destinationMasked, final String provider,
final String providerReference, final String failureReason, final String messageBody) {
this.deliveryStatus = status;
this.destinationMasked = destinationMasked;
this.provider = provider;
this.providerReference = providerReference;
this.failureReason = truncate(failureReason, 500);
this.messageBody = truncate(messageBody, 2000);
}
private static String truncate(final String value, final int max) {
if (value == null || value.length() <= max) {
return value;
}
return value.substring(0, max - 3) + "...";
}
public Long getId() {
return this.id;
}
public Client getClient() {
return this.client;
}
public Long getKycId() {
return this.kycId;
}
public KycContactChannel getChannel() {
return this.channel;
}
public String getNotes() {
return this.notes;
}
public String getOfficerName() {
return this.officerName;
}
public Long getLoggedByUserId() {
return this.loggedByUserId;
}
public String getExceptionReason() {
return this.exceptionReason;
}
public KycContactDeliveryStatus getDeliveryStatus() {
return this.deliveryStatus;
}
public String getDestinationMasked() {
return this.destinationMasked;
}
public String getProvider() {
return this.provider;
}
public String getProviderReference() {
return this.providerReference;
}
public String getFailureReason() {
return this.failureReason;
}
public String getMessageBody() {
return this.messageBody;
}
public LocalDateTime getContactedAt() {
return this.contactedAt;
}
public LocalDateTime getCreatedAt() {
return this.createdAt;
}
}
@@ -0,0 +1,36 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.domain;
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 ClientKycContactLogRepository extends JpaRepository<ClientKycContactLog, Long> {
List<ClientKycContactLog> findByClientIdOrderByContactedAtAsc(Long clientId);
/**
* Every contact log for the given clients in one round trip, so the exceptions grid can render
* "last contacted" for the whole page without an N+1 query per row.
*/
@Query("SELECT l FROM ClientKycContactLog l WHERE l.client.id IN :clientIds ORDER BY l.contactedAt ASC")
List<ClientKycContactLog> findByClientIds(@Param("clientIds") List<Long> clientIds);
}
@@ -33,6 +33,18 @@ public interface ClientKycRepository extends JpaRepository<ClientKyc, Long> {
boolean existsByClientId(Long clientId); boolean existsByClientId(Long clientId);
List<ClientKyc> findByIsExceptionTrueAndExceptionResolvedFalse();
/**
* Resolved exceptions, keyed on {@code exceptionResolved} alone.
*
* <p>Deliberately does NOT also require {@code isException = true}. Resolving an exception
* clears {@code isException} — that flag means "has an OPEN exception", so leaving it set on a
* closed one would be wrong. An earlier version of this query required both flags, which made
* every resolved record invisible in both tabs the moment it was resolved.
*/
List<ClientKyc> findByExceptionResolvedTrue();
@Query("SELECT k FROM ClientKyc k WHERE k.kycStatus = :status") @Query("SELECT k FROM ClientKyc k WHERE k.kycStatus = :status")
List<ClientKyc> findAllByStatus(@Param("status") Integer status); List<ClientKyc> findAllByStatus(@Param("status") Integer status);
@@ -55,12 +55,13 @@ 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
* Count clients whose cif_number begins with the given prefix. Used by the CIF number generator @Query("UPDATE Client client SET client.signatureId = :signatureId WHERE client.id = :clientId")
* (TTYYYYNNNNNN format) to compute the next sequence within a given (type, year) bucket. void updateByIdAndSignatureId(@Param("clientId") Long clientId, @Param("signatureId") Long signatureId);
*/
@Query("SELECT COUNT(c) FROM Client c WHERE c.cifNumber LIKE CONCAT(:prefix, '%')") @Modifying
long countByCifNumberPrefix(@Param("prefix") String prefix); @Query("UPDATE Client client SET client.signatureId = null WHERE client.id = :clientId")
void removeSignatureId(@Param("clientId") Long clientId);
// ── Pre-save uniqueness probes ─────────────────────────────────────────── // ── Pre-save uniqueness probes ───────────────────────────────────────────
// Why: DB has UNIQUE on mobile_no / email_address, but we want to fail fast with a clear // Why: DB has UNIQUE on mobile_no / email_address, but we want to fail fast with a clear
@@ -0,0 +1,211 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* 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() {}
/**
* Builds a rule authored from the AML Rules Master screen. Occupation and occupation-category
* are attached separately by the caller, which owns the CodeValue lookup.
*/
public static ClientRiskRule create(final String ruleName, final String reason, final Integer riskCategory,
final Integer orderPosition, final Integer legalFormId, final BigDecimal minMonthlyIncome,
final BigDecimal maxMonthlyIncome, final BigDecimal minAnnualIncome, final BigDecimal maxAnnualIncome,
final BigDecimal minAnnualTurnover, final BigDecimal maxAnnualTurnover, final String amlCheckStatus,
final String bannedEntityMatch, final String willfulDefaulter, final String ecgcSalFlag,
final String dormantFlag) {
final ClientRiskRule rule = new ClientRiskRule();
rule.ruleName = ruleName;
rule.reason = reason;
rule.riskCategory = riskCategory;
rule.orderPosition = orderPosition;
rule.legalFormId = legalFormId;
rule.minMonthlyIncome = minMonthlyIncome;
rule.maxMonthlyIncome = maxMonthlyIncome;
rule.minAnnualIncome = minAnnualIncome;
rule.maxAnnualIncome = maxAnnualIncome;
rule.minAnnualTurnover = minAnnualTurnover;
rule.maxAnnualTurnover = maxAnnualTurnover;
rule.amlCheckStatus = amlCheckStatus;
rule.bannedEntityMatch = bannedEntityMatch;
rule.willfulDefaulter = willfulDefaulter;
rule.ecgcSalFlag = ecgcSalFlag;
rule.dormantFlag = dormantFlag;
return rule;
}
public void setOccupationCategory(final CodeValue occupationCategory) {
this.occupationCategory = occupationCategory;
}
public void setOccupation(final CodeValue occupation) {
this.occupation = occupation;
}
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,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.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();
/** Highest position in use, so a new rule can be appended to the end of the catalog. */
@Query("SELECT MAX(r.orderPosition) FROM ClientRiskRule r")
Integer findMaxOrderPosition();
boolean existsByRuleNameIgnoreCase(String ruleName);
}
@@ -0,0 +1,91 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.domain;
/**
* The channel a KYC officer used (or asked the platform to use) to reach a customer about an open
* KYC exception.
*
* <p>The critical distinction is {@link #isPlatformSent()}:
* <ul>
* <li><b>Offline channels</b> ({@code PHONE_CALL}, {@code BRANCH_VISIT}, {@code FIELD_VISIT}) are
* recorded after the fact — the officer already did the thing, the platform only writes it
* down.</li>
* <li><b>Sending channels</b> ({@code SMS}, {@code EMAIL}, {@code WHATSAPP}) cause the platform to
* actually transmit a notice to the customer at save time.</li>
* </ul>
* Mixing these up would let a "logged" row imply the customer was told something they never
* received, so every call site branches on this flag rather than on the channel name.
*/
public enum KycContactChannel {
PHONE_CALL("Phone Call", false),
SMS("SMS", true),
EMAIL("Email", true),
WHATSAPP("WhatsApp Message", true),
BRANCH_VISIT("In-Branch Visit", false),
FIELD_VISIT("Agent Field Visit", false);
private final String displayLabel;
private final boolean platformSent;
KycContactChannel(final String displayLabel, final boolean platformSent) {
this.displayLabel = displayLabel;
this.platformSent = platformSent;
}
public String getDisplayLabel() {
return this.displayLabel;
}
/** True when saving a log of this channel must actually transmit a message to the customer. */
public boolean isPlatformSent() {
return this.platformSent;
}
/**
* Lenient parse of whatever the UI sent. Accepts the enum name ({@code SMS}) as well as the
* display label, and tolerates the descriptive suffixes the dialog uses — e.g.
* {@code "SMS (Aadhaar OTP request)"} and {@code "Email (PAN Link request)"} — because those
* labels describe the *purpose* of the message, not a different channel.
*
* @return the matching channel, or {@code null} when nothing matches.
*/
public static KycContactChannel fromString(final String raw) {
if (raw == null || raw.isBlank()) {
return null;
}
final String normalised = raw.trim().toUpperCase();
for (final KycContactChannel channel : values()) {
if (normalised.equals(channel.name()) || normalised.equals(channel.displayLabel.toUpperCase())) {
return channel;
}
}
// Prefix match handles the "(…request)" suffixes. Longest label first so WHATSAPP's
// "WHATSAPP MESSAGE" is preferred over a shorter accidental prefix.
KycContactChannel best = null;
for (final KycContactChannel channel : values()) {
final String label = channel.displayLabel.toUpperCase();
if (normalised.startsWith(label) && (best == null || label.length() > best.displayLabel.length())) {
best = channel;
}
}
return best;
}
}
@@ -0,0 +1,48 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.domain;
/**
* Outcome of a platform-sent KYC contact notice.
*
* <p>Every state other than {@link #SENT} means the customer was <em>not</em> reached. The officer
* is shown the distinction explicitly, because a contact log that silently swallows a gateway
* failure would understate the bank's outreach obligation during a KYC audit.
*/
public enum KycContactDeliveryStatus {
/** Offline channel — nothing was transmitted and nothing was supposed to be. */
NOT_APPLICABLE,
/** The gateway accepted the message for delivery. */
SENT,
/** The gateway was reached but rejected the message, or the call errored. */
FAILED,
/** The customer has no mobile number / email address on file for this channel. */
SKIPPED_NO_DESTINATION,
/** No gateway is configured for this channel in this deployment. */
PROVIDER_NOT_CONFIGURED;
public boolean isDelivered() {
return this == SENT;
}
}
@@ -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.
@@ -64,6 +65,29 @@ public class MobileLookupLog extends AbstractAuditableWithUTCDateTimeCustom<Long
@Column(name = "looked_up_on", nullable = false) @Column(name = "looked_up_on", nullable = false)
private OffsetDateTime lookedUpOn; private OffsetDateTime lookedUpOn;
// ── Officer decision on a non-clean result ────────────────────────────────
// A DIP hit is a fraud-risk signal rather than a legal bar, so onboarding may continue — but
// the officer has to say why in writing, and that reasoning is kept with the lookup.
/** PROCEEDED | REJECTED. Null while no decision is required or none has been taken. */
@Column(name = "override_decision", length = 20)
private String overrideDecision;
@Column(name = "override_reason")
private String overrideReason;
@Column(name = "decided_by")
private Long decidedBy;
@Column(name = "decided_on")
private OffsetDateTime decidedOn;
public static final String STATUS_CLEAN = "CLEAN";
public static final String STATUS_ERROR = "ERROR";
public static final String STATUS_SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE";
public static final String DECISION_PROCEEDED = "PROCEEDED";
public static final String DECISION_REJECTED = "REJECTED";
protected MobileLookupLog() {} protected MobileLookupLog() {}
public MobileLookupLog(final String mobileNumber, final String lookupStatus, public MobileLookupLog(final String mobileNumber, final String lookupStatus,
@@ -78,6 +102,29 @@ 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();
}
public void recordDecision(final String decision, final String reason, final Long userId) {
this.overrideDecision = decision;
this.overrideReason = reason;
this.decidedBy = userId;
this.decidedOn = DateUtils.getOffsetDateTimeOfTenant();
}
/** True when the lookup produced something the officer must consciously decide about. */
public boolean requiresDecision() {
return this.suspected || this.disconnected;
}
/**
* A number may be used only when it came back clean, or when an officer explicitly chose to
* proceed. An unreachable DoT service is NOT a pass — it means nothing was checked.
*/
public boolean isUsable() {
if (STATUS_CLEAN.equals(this.lookupStatus)) {
return true;
}
return DECISION_PROCEEDED.equals(this.overrideDecision);
} }
} }
@@ -19,6 +19,8 @@
package org.apache.fineract.portfolio.mobilelookup.domain; package org.apache.fineract.portfolio.mobilelookup.domain;
import java.util.List; import java.util.List;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
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;
@@ -35,4 +37,29 @@ public interface MobileLookupLogRepository extends JpaRepository<MobileLookupLog
@Query("SELECT l FROM MobileLookupLog l WHERE l.lookupStatus = :status ORDER BY l.lookedUpOn DESC") @Query("SELECT l FROM MobileLookupLog l WHERE l.lookupStatus = :status ORDER BY l.lookedUpOn DESC")
List<MobileLookupLog> findByLookupStatus(@Param("status") String status); List<MobileLookupLog> findByLookupStatus(@Param("status") String status);
/** Non-clean lookups still waiting for an officer's decision — the DIP review queue. */
@Query("SELECT l FROM MobileLookupLog l WHERE (l.suspected = true OR l.disconnected = true) "
+ "AND l.overrideDecision IS NULL ORDER BY l.lookedUpOn DESC")
List<MobileLookupLog> findPendingDecisions();
/**
* Most recent lookup for a number, used by the server-side gate to confirm the browser's
* claim that a number was verified.
*
* <p>
* Paged rather than a JPQL {@code LIMIT}, which EclipseLink does not accept — the caller
* takes the first row.
* </p>
*/
@Query("SELECT l FROM MobileLookupLog l WHERE l.mobileNumber = :mobileNumber ORDER BY l.lookedUpOn DESC")
List<MobileLookupLog> findLatestByMobileNumber(@Param("mobileNumber") String mobileNumber, Pageable pageable);
/**
* Earlier rejections of the same number, on any client. A number turned away once is a
* standing signal: without this the next officer screens it fresh and sees no history.
*/
@Query("SELECT l FROM MobileLookupLog l WHERE l.mobileNumber = :mobileNumber "
+ "AND l.overrideDecision = 'REJECTED' ORDER BY l.decidedOn DESC")
List<MobileLookupLog> findRejections(@Param("mobileNumber") String mobileNumber, Pageable pageable);
} }
@@ -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.
@@ -60,6 +61,65 @@ public class SanctionsScreeningLog extends AbstractAuditableWithUTCDateTimeCusto
@Column(name = "screened_on", nullable = false) @Column(name = "screened_on", nullable = false)
private OffsetDateTime screenedOn; private OffsetDateTime screenedOn;
// ── Alert disposition ─────────────────────────────────────────────────────
// A FLAGGED screening opens a case. It stays PENDING_REVIEW until an officer either
// documents it as a different person (FALSE_POSITIVE, effective only once the Principal
// Officer approves) or confirms it (CONFIRMED_MATCH, which freezes and gets reported).
@Column(name = "disposition", length = 30)
private String disposition;
@Column(name = "disposition_reason")
private String dispositionReason;
@Column(name = "reviewed_by")
private Long reviewedBy;
@Column(name = "reviewed_on")
private OffsetDateTime reviewedOn;
@Column(name = "approved_by")
private Long approvedBy;
@Column(name = "approved_on")
private OffsetDateTime approvedOn;
@Column(name = "fiu_str_reference", length = 100)
private String fiuStrReference;
@Column(name = "nodal_officer_informed_on")
private OffsetDateTime nodalOfficerInformedOn;
@Column(name = "frozen_account_ids", length = 1000)
private String frozenAccountIds;
@Column(name = "frozen_on")
private OffsetDateTime frozenOn;
// ── STR tracking ──────────────────────────────────────────────────────────
// A confirmed match is reportable to FIU-IND. Tracked here rather than as an AML alert: an
// AML alert is built around a monetary transaction, whereas this is triggered by who the
// customer is, so the two are surfaced as separate streams on the FIU screen.
/** NULL until approval; then PENDING_FILING → FILED. */
@Column(name = "str_status", length = 30)
private String strStatus;
@Column(name = "str_due_date")
private java.time.LocalDate strDueDate;
@Column(name = "str_raised_on")
private OffsetDateTime strRaisedOn;
public static final String STR_PENDING_FILING = "PENDING_FILING";
public static final String STR_FILED = "FILED";
public static final String RESULT_FLAGGED = "FLAGGED";
public static final String RESULT_CLEAR = "CLEAR";
public static final String DISPOSITION_PENDING = "PENDING_REVIEW";
public static final String DISPOSITION_FALSE_POSITIVE = "FALSE_POSITIVE";
public static final String DISPOSITION_CONFIRMED = "CONFIRMED_MATCH";
protected SanctionsScreeningLog() {} protected SanctionsScreeningLog() {}
public SanctionsScreeningLog(final String screenedName, final String screeningResult, public SanctionsScreeningLog(final String screenedName, final String screeningResult,
@@ -73,9 +133,76 @@ 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();
// A hit opens a case; a clear screening has no case to close.
this.disposition = RESULT_FLAGGED.equals(screeningResult) ? DISPOSITION_PENDING : null;
} }
// ── Alert lifecycle ───────────────────────────────────────────────────────
/**
* Records the officer's assessment of the hit. Deliberately does NOT clear the block: a
* false positive only takes effect once {@link #approve(Long)} is called by the Principal
* Officer, so the person who raised it cannot wave their own match through.
*/
public void recordDisposition(final String newDisposition, final String reason, final Long userId) {
this.disposition = newDisposition;
this.dispositionReason = reason;
this.reviewedBy = userId;
this.reviewedOn = DateUtils.getOffsetDateTimeOfTenant();
// Re-opening a previously approved alert must drop the old approval with it.
this.approvedBy = null;
this.approvedOn = null;
}
public void approve(final Long approverId) {
this.approvedBy = approverId;
this.approvedOn = DateUtils.getOffsetDateTimeOfTenant();
}
/** Records the FIU-IND STR reference and the date the nodal officer was informed. */
public void recordReporting(final String strReference, final OffsetDateTime nodalOfficerInformedOn) {
if (strReference != null) {
this.fiuStrReference = strReference;
// Recording the FIU-IND acknowledgement is what closes the filing obligation, so the
// case stops appearing as outstanding on the FIU screen.
if (!strReference.isBlank()) {
this.strStatus = STR_FILED;
}
}
if (nodalOfficerInformedOn != null) {
this.nodalOfficerInformedOn = nodalOfficerInformedOn;
}
}
public void recordFreeze(final String accountIds) {
this.frozenAccountIds = accountIds;
this.frozenOn = DateUtils.getOffsetDateTimeOfTenant();
}
/**
* Opens the STR obligation on approval of a confirmed match. PMLA allows seven working days
* from forming the suspicion; seven calendar days is the stricter reading.
*/
public void raiseStr() {
if (this.strStatus != null) {
return;
}
this.strStatus = STR_PENDING_FILING;
this.strRaisedOn = DateUtils.getOffsetDateTimeOfTenant();
this.strDueDate = DateUtils.getBusinessLocalDate().plusDays(7);
}
public boolean isStrPending() { return STR_PENDING_FILING.equals(this.strStatus); }
public boolean isFlagged() { return RESULT_FLAGGED.equals(this.screeningResult); }
public boolean isApprovedFalsePositive() {
return DISPOSITION_FALSE_POSITIVE.equals(this.disposition) && this.approvedBy != null;
}
public boolean isConfirmedMatch() { return DISPOSITION_CONFIRMED.equals(this.disposition); }
// ── Accessors ───────────────────────────────────────────────────────────── // ── Accessors ─────────────────────────────────────────────────────────────
public String getScreenedName() { return screenedName; } public String getScreenedName() { return screenedName; }
@@ -88,4 +215,17 @@ public class SanctionsScreeningLog extends AbstractAuditableWithUTCDateTimeCusto
public void setClientId(Long clientId) { this.clientId = clientId; } public void setClientId(Long clientId) { this.clientId = clientId; }
public String getIpAddress() { return ipAddress; } public String getIpAddress() { return ipAddress; }
public OffsetDateTime getScreenedOn() { return screenedOn; } public OffsetDateTime getScreenedOn() { return screenedOn; }
public String getDisposition() { return disposition; }
public String getDispositionReason() { return dispositionReason; }
public Long getReviewedBy() { return reviewedBy; }
public OffsetDateTime getReviewedOn() { return reviewedOn; }
public Long getApprovedBy() { return approvedBy; }
public OffsetDateTime getApprovedOn() { return approvedOn; }
public String getFiuStrReference() { return fiuStrReference; }
public OffsetDateTime getNodalOfficerInformedOn() { return nodalOfficerInformedOn; }
public String getFrozenAccountIds() { return frozenAccountIds; }
public OffsetDateTime getFrozenOn() { return frozenOn; }
public String getStrStatus() { return strStatus; }
public java.time.LocalDate getStrDueDate() { return strDueDate; }
public OffsetDateTime getStrRaisedOn() { return strRaisedOn; }
} }
@@ -39,4 +39,32 @@ public interface SanctionsScreeningLogRepository extends JpaRepository<Sanctions
@Query("SELECT l FROM SanctionsScreeningLog l WHERE LOWER(l.screenedName) LIKE LOWER(CONCAT('%', :name, '%')) ORDER BY l.screenedOn DESC") @Query("SELECT l FROM SanctionsScreeningLog l WHERE LOWER(l.screenedName) LIKE LOWER(CONCAT('%', :name, '%')) ORDER BY l.screenedOn DESC")
List<SanctionsScreeningLog> findByScreenedNameContaining(@Param("name") String name); List<SanctionsScreeningLog> findByScreenedNameContaining(@Param("name") String name);
@Query("SELECT l FROM SanctionsScreeningLog l WHERE l.disposition = :disposition ORDER BY l.screenedOn DESC")
List<SanctionsScreeningLog> findByDisposition(@Param("disposition") String disposition);
/** Every open alert — the Principal Officer's review queue. */
@Query("SELECT l FROM SanctionsScreeningLog l WHERE l.disposition IS NOT NULL ORDER BY l.screenedOn DESC")
List<SanctionsScreeningLog> findAllAlerts();
/**
* A name is only cleared to onboard once a false positive on that exact name has been
* approved by the Principal Officer — the maker's own assessment is not enough.
*/
@Query("SELECT COUNT(l) FROM SanctionsScreeningLog l WHERE LOWER(TRIM(l.screenedName)) = LOWER(TRIM(:name)) "
+ "AND l.disposition = 'FALSE_POSITIVE' AND l.approvedBy IS NOT NULL")
long countApprovedFalsePositives(@Param("name") String name);
/** Latest alert for a client, used to decide whether their accounts must stay frozen. */
@Query("SELECT l FROM SanctionsScreeningLog l WHERE l.clientId = :clientId AND l.screeningResult = 'FLAGGED' "
+ "ORDER BY l.screenedOn DESC")
List<SanctionsScreeningLog> findFlaggedByClientId(@Param("clientId") Long clientId);
/**
* Confirmed matches carrying an STR obligation — the sanctions stream on the FIU-IND screen.
* Pass null for every STR regardless of whether it has been filed.
*/
@Query("SELECT l FROM SanctionsScreeningLog l WHERE l.strStatus IS NOT NULL "
+ "AND (:status IS NULL OR l.strStatus = :status) ORDER BY l.strDueDate ASC, l.strRaisedOn DESC")
List<SanctionsScreeningLog> findStrCases(@Param("status") String status);
} }
@@ -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() {}
}
@@ -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
@@ -54,6 +54,7 @@
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class> <class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
<class>org.apache.fineract.portfolio.group.domain.Group</class> <class>org.apache.fineract.portfolio.group.domain.Group</class>
<class>org.apache.fineract.portfolio.client.domain.Client</class> <class>org.apache.fineract.portfolio.client.domain.Client</class>
<class>org.apache.fineract.portfolio.client.domain.ClientKyc</class>
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class> <class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class>
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class> <class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class> <class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
@@ -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));
}
}

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