Add all CBS custom modules: KYC, Sanctions, RBI Feed, OpenProject, Mobile Lookup, Charges, Avro schemas
Includes all prior work not yet committed on this branch: - KYC module (LincPay eKYC: PAN, Aadhaar OTP, GSTIN verification) - Sanctions screening (UN Security Council consolidated list) - RBI Feed integration with scheduled jobs - OpenProject API integration - Mobile lookup service - Charge module enhancements (current account support) - Client family member nominee fields - Avro schema updates - Liquibase migrations 0216-0224 - Postman collections for OpenProject, RBI Feeds, Sanctions - SQL seed scripts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
78f37257a7
commit
ba5365d4af
@@ -0,0 +1,273 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "op-fineract-collection",
|
||||||
|
"name": "Fineract - OpenProject Integration",
|
||||||
|
"description": "Postman collection for Fineract OpenProject integration API endpoints.\n\nBase URL: {{baseUrl}}/fineract-provider/api/v1/openproject\n\nAuthentication: Basic Auth (Fineract user credentials)",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"type": "basic",
|
||||||
|
"basic": [
|
||||||
|
{
|
||||||
|
"key": "username",
|
||||||
|
"value": "{{fineractUser}}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "password",
|
||||||
|
"value": "{{fineractPassword}}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "baseUrl",
|
||||||
|
"value": "https://localhost:8443",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "fineractUser",
|
||||||
|
"value": "mifos",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "fineractPassword",
|
||||||
|
"value": "password",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "tenantId",
|
||||||
|
"value": "default",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get Project Details",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "fineract-platform-tenantid",
|
||||||
|
"value": "{{tenantId}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/openproject/project",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "openproject", "project"]
|
||||||
|
},
|
||||||
|
"description": "Returns the configured OpenProject project details (CBSNexora)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Work Packages (All)",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "fineract-platform-tenantid",
|
||||||
|
"value": "{{tenantId}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/openproject/work-packages?pageSize=25&offset=1",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "openproject", "work-packages"],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "pageSize",
|
||||||
|
"value": "25"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "offset",
|
||||||
|
"value": "1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Returns all work packages from the OpenProject project."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Work Packages (Open)",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "fineract-platform-tenantid",
|
||||||
|
"value": "{{tenantId}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/openproject/work-packages?status=open&pageSize=25&offset=1",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "openproject", "work-packages"],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "status",
|
||||||
|
"value": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pageSize",
|
||||||
|
"value": "25"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "offset",
|
||||||
|
"value": "1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Returns only open work packages."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Work Packages (Closed)",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "fineract-platform-tenantid",
|
||||||
|
"value": "{{tenantId}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/openproject/work-packages?status=closed&pageSize=25&offset=1",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "openproject", "work-packages"],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "status",
|
||||||
|
"value": "closed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pageSize",
|
||||||
|
"value": "25"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "offset",
|
||||||
|
"value": "1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Returns only closed work packages."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Status Summary",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "fineract-platform-tenantid",
|
||||||
|
"value": "{{tenantId}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/openproject/summary",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "openproject", "summary"]
|
||||||
|
},
|
||||||
|
"description": "Returns a summary of all work packages grouped by status, type, and priority."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create Work Package - Feature",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "fineract-platform-tenantid",
|
||||||
|
"value": "{{tenantId}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"subject\": \"Integrate OpenProject with CBS Dashboard\",\n \"description\": \"Add OpenProject work package tracking to the CBS admin dashboard so project managers can view task progress directly from the banking system.\",\n \"type\": \"Feature\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/openproject/work-packages",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "openproject", "work-packages"]
|
||||||
|
},
|
||||||
|
"description": "Creates a new Feature work package in OpenProject.\n\nValid types: Task, Milestone, Summary task, Feature, Epic, User story, Bug, Incident, Support Ticket, Problem, Change Request, Enhancement"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create Work Package - Task",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "fineract-platform-tenantid",
|
||||||
|
"value": "{{tenantId}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"subject\": \"Setup CI/CD pipeline for CBS deployment\",\n \"description\": \"Configure automated build and deployment pipeline for the Core Banking System.\",\n \"type\": \"Task\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/openproject/work-packages",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "openproject", "work-packages"]
|
||||||
|
},
|
||||||
|
"description": "Creates a new Task work package in OpenProject."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create Work Package - Bug",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "fineract-platform-tenantid",
|
||||||
|
"value": "{{tenantId}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"subject\": \"Fix loan repayment calculation rounding error\",\n \"description\": \"Loan repayment amounts show minor rounding differences when calculated with different interest methods.\",\n \"type\": \"Bug\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/openproject/work-packages",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "openproject", "work-packages"]
|
||||||
|
},
|
||||||
|
"description": "Creates a new Bug work package in OpenProject."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "rbi-feeds-collection",
|
||||||
|
"name": "RBI Feeds API - Fineract CBS",
|
||||||
|
"description": "RBI RSS Feed endpoints for fetching and retrieving RBI circulars, notifications, press releases etc.",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"type": "basic",
|
||||||
|
"basic": [
|
||||||
|
{
|
||||||
|
"key": "username",
|
||||||
|
"value": "mifos",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "password",
|
||||||
|
"value": "password",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "baseUrl",
|
||||||
|
"value": "https://localhost:8443/fineract-provider/api/v1",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "tenantId",
|
||||||
|
"value": "default",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "1. Trigger Fetch (Run First)",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/rbi-feeds/fetch",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["rbi-feeds", "fetch"]
|
||||||
|
},
|
||||||
|
"description": "Fetches latest RSS feeds from rbi.org.in (5 categories: PRESS_RELEASE, NOTIFICATION, SPEECH, TENDER, PUBLICATION) and stores new items in the database. Run this FIRST to populate data."
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "2. Get All Feeds",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/rbi-feeds",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["rbi-feeds"]
|
||||||
|
},
|
||||||
|
"description": "Retrieve all RBI feed items sorted by publication date (newest first)."
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "3. Get Feeds by Category - PRESS_RELEASE",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/rbi-feeds?category=PRESS_RELEASE",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["rbi-feeds"],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "category",
|
||||||
|
"value": "PRESS_RELEASE"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Filter feeds by PRESS_RELEASE category."
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "4. Get Feeds by Category - NOTIFICATION",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/rbi-feeds?category=NOTIFICATION",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["rbi-feeds"],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "category",
|
||||||
|
"value": "NOTIFICATION"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Filter feeds by NOTIFICATION category."
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "5. Get Feeds by Category - SPEECH",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/rbi-feeds?category=SPEECH",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["rbi-feeds"],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "category",
|
||||||
|
"value": "SPEECH"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Filter feeds by SPEECH category."
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "6. Get Feeds by Category - TENDER",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/rbi-feeds?category=TENDER",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["rbi-feeds"],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "category",
|
||||||
|
"value": "TENDER"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Filter feeds by TENDER category."
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "7. Get Feeds by Category - PUBLICATION",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/rbi-feeds?category=PUBLICATION",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["rbi-feeds"],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "category",
|
||||||
|
"value": "PUBLICATION"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Filter feeds by PUBLICATION category."
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "8. Get Single Feed by ID",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/rbi-feeds/1",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["rbi-feeds", "1"]
|
||||||
|
},
|
||||||
|
"description": "Retrieve a single RBI feed item by its ID. Change the ID in the URL as needed."
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "sanctions-screening-001",
|
||||||
|
"name": "Sanctions Screening API",
|
||||||
|
"description": "API collection for UN Security Council consolidated sanctions list screening.\n\nEndpoints:\n- Screen client names against sanctions list\n- Load sanctions XML data\n- Check sanctions database status\n\nBase URL: {{baseUrl}}/fineract-provider/api/v1",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"type": "basic",
|
||||||
|
"basic": [
|
||||||
|
{
|
||||||
|
"key": "username",
|
||||||
|
"value": "{{username}}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "password",
|
||||||
|
"value": "{{password}}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "baseUrl",
|
||||||
|
"value": "https://localhost:8443"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "username",
|
||||||
|
"value": "mifos"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "password",
|
||||||
|
"value": "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "tenantId",
|
||||||
|
"value": "default"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Sanctions Screening",
|
||||||
|
"description": "Screen client names against the UN Security Council consolidated sanctions list",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Screen Name (GET)",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/screen?name=Mohammad Ali",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "sanctions", "screen"],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "name",
|
||||||
|
"value": "Mohammad Ali",
|
||||||
|
"description": "Name to screen against sanctions list"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Screen a client name against the UN Security Council consolidated sanctions list using GET method.\n\n**Response when match found (FLAGGED):**\n```json\n{\n \"screenedName\": \"Mohammad Ali\",\n \"matchFound\": true,\n \"status\": \"FLAGGED\",\n \"message\": \"Potential match found in UN Security Council consolidated sanctions list. Manual review required before proceeding.\",\n \"matchCount\": 2,\n \"matches\": [...]\n}\n```\n\n**Response when no match (CLEAR):**\n```json\n{\n \"screenedName\": \"John Smith\",\n \"matchFound\": false,\n \"status\": \"CLEAR\",\n \"message\": \"No match found in UN Security Council consolidated sanctions list.\",\n \"matchCount\": 0,\n \"matches\": []\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Screen Name (POST)",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"name\": \"AL-QAIDA\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/screen",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "sanctions", "screen"]
|
||||||
|
},
|
||||||
|
"description": "Screen a client name against sanctions list using POST method with JSON body."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Screen Name - Individual (FLAGGED example)",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"name\": \"ERIC BADEGE\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/screen",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "sanctions", "screen"]
|
||||||
|
},
|
||||||
|
"description": "Example: Screen a known sanctioned individual name. Should return FLAGGED status."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Screen Name - Clear (no match)",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"name\": \"Aditya Kumar\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/screen",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "sanctions", "screen"]
|
||||||
|
},
|
||||||
|
"description": "Example: Screen a name that does not exist in sanctions list. Should return CLEAR status."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Screen Name - Entity (FLAGGED example)",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"name\": \"TALIBAN\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/screen",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "sanctions", "screen"]
|
||||||
|
},
|
||||||
|
"description": "Example: Screen a known sanctioned entity name. Should return FLAGGED status."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sanctions Management",
|
||||||
|
"description": "Load and manage sanctions data",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get Sanctions Status",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/status",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "sanctions", "status"]
|
||||||
|
},
|
||||||
|
"description": "Get the count of active sanctions entries in the database.\n\n**Response:**\n```json\n{\n \"totalActiveEntries\": 1002,\n \"status\": \"ACTIVE\"\n}\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Load Sanctions XML",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/xml",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "file",
|
||||||
|
"file": {
|
||||||
|
"src": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/load",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "sanctions", "load"]
|
||||||
|
},
|
||||||
|
"description": "Upload and load the UN consolidated sanctions XML file into the database.\n\nSelect the `consolidatedLegacyByPRN.xml` file in the Body > Binary section.\n\n**Response:**\n```json\n{\n \"message\": \"Sanctions data loaded successfully\",\n \"entriesLoaded\": 1002,\n \"totalActive\": 1002\n}\n```"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Screening Activity Logs",
|
||||||
|
"description": "View sanctions screening activity logs — every search is recorded",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get All Screening Logs",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/logs",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "sanctions", "logs"]
|
||||||
|
},
|
||||||
|
"description": "Get all sanctions screening activity logs (most recent first).\n\n**Response:**\n```json\n[\n {\n \"id\": 1,\n \"screenedName\": \"ERIC BADEGE\",\n \"screeningResult\": \"FLAGGED\",\n \"matchCount\": 1,\n \"matchedNames\": \"ERIC BADEGE\",\n \"screeningSource\": \"CLIENT_CREATION\",\n \"screenedOn\": \"2026-03-16T10:30:00Z\",\n \"screenedBy\": 1\n }\n]\n```"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get FLAGGED Logs Only",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/logs?result=FLAGGED",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "sanctions", "logs"],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "result",
|
||||||
|
"value": "FLAGGED",
|
||||||
|
"description": "Filter by result: FLAGGED or CLEAR"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Get only FLAGGED screening logs — shows all searches that matched a sanctioned entry."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get CLEAR Logs Only",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/logs?result=CLEAR",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "sanctions", "logs"],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "result",
|
||||||
|
"value": "CLEAR",
|
||||||
|
"description": "Filter by result: FLAGGED or CLEAR"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Get only CLEAR screening logs — shows all searches that did not match any sanctioned entry."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Search Logs by Name",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Fineract-Platform-TenantId",
|
||||||
|
"value": "{{tenantId}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/fineract-provider/api/v1/sanctions/logs?name=ERIC",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["fineract-provider", "api", "v1", "sanctions", "logs"],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "name",
|
||||||
|
"value": "ERIC",
|
||||||
|
"description": "Search logs by screened name (partial match)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Search screening logs by the name that was screened (partial, case-insensitive match)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+10
-2
@@ -24,7 +24,8 @@ public enum ChargeAppliesTo {
|
|||||||
LOAN(1, "chargeAppliesTo.loan"), //
|
LOAN(1, "chargeAppliesTo.loan"), //
|
||||||
SAVINGS(2, "chargeAppliesTo.savings"), //
|
SAVINGS(2, "chargeAppliesTo.savings"), //
|
||||||
CLIENT(3, "chargeAppliesTo.client"), //
|
CLIENT(3, "chargeAppliesTo.client"), //
|
||||||
SHARES(4, "chargeAppliesTo.shares"); //
|
SHARES(4, "chargeAppliesTo.shares"), //
|
||||||
|
CURRENT_ACCOUNT(5, "chargeAppliesTo.currentAccount"); //
|
||||||
|
|
||||||
private final Integer value;
|
private final Integer value;
|
||||||
private final String code;
|
private final String code;
|
||||||
@@ -59,6 +60,9 @@ public enum ChargeAppliesTo {
|
|||||||
case 4:
|
case 4:
|
||||||
chargeAppliesToType = SHARES;
|
chargeAppliesToType = SHARES;
|
||||||
break;
|
break;
|
||||||
|
case 5:
|
||||||
|
chargeAppliesToType = CURRENT_ACCOUNT;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
chargeAppliesToType = INVALID;
|
chargeAppliesToType = INVALID;
|
||||||
break;
|
break;
|
||||||
@@ -84,8 +88,12 @@ public enum ChargeAppliesTo {
|
|||||||
return this.value.equals(SHARES.getValue());
|
return this.value.equals(SHARES.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isCurrentAccountCharge() {
|
||||||
|
return this.value.equals(CURRENT_ACCOUNT.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
public static Object[] validValues() {
|
public static Object[] validValues() {
|
||||||
return new Object[] { ChargeAppliesTo.LOAN.getValue(), ChargeAppliesTo.SAVINGS.getValue(), ChargeAppliesTo.CLIENT.getValue(),
|
return new Object[] { ChargeAppliesTo.LOAN.getValue(), ChargeAppliesTo.SAVINGS.getValue(), ChargeAppliesTo.CLIENT.getValue(),
|
||||||
ChargeAppliesTo.SHARES.getValue() };
|
ChargeAppliesTo.SHARES.getValue(), ChargeAppliesTo.CURRENT_ACCOUNT.getValue() };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -53,6 +53,10 @@ public enum ChargeCalculationType {
|
|||||||
return new Integer[] { ChargeCalculationType.FLAT.getValue(), ChargeCalculationType.PERCENT_OF_AMOUNT.getValue() };
|
return new Integer[] { ChargeCalculationType.FLAT.getValue(), ChargeCalculationType.PERCENT_OF_AMOUNT.getValue() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Object[] validValuesForCurrentAccount() {
|
||||||
|
return new Integer[] { ChargeCalculationType.FLAT.getValue(), ChargeCalculationType.PERCENT_OF_AMOUNT.getValue() };
|
||||||
|
}
|
||||||
|
|
||||||
public static Object[] validValuesForShares() {
|
public static Object[] validValuesForShares() {
|
||||||
return new Integer[] { ChargeCalculationType.FLAT.getValue(), ChargeCalculationType.PERCENT_OF_AMOUNT.getValue() };
|
return new Integer[] { ChargeCalculationType.FLAT.getValue(), ChargeCalculationType.PERCENT_OF_AMOUNT.getValue() };
|
||||||
}
|
}
|
||||||
|
|||||||
+32
@@ -255,6 +255,38 @@ public final class ChargeDefinitionCommandFromApiJsonDeserializer {
|
|||||||
baseDataValidator.reset().parameter(CHARGE_CALCULATION_TYPE).value(chargeCalculationType)
|
baseDataValidator.reset().parameter(CHARGE_CALCULATION_TYPE).value(chargeCalculationType)
|
||||||
.isOneOfTheseValues(ChargeCalculationType.validValuesForShareAccountActivation());
|
.isOneOfTheseValues(ChargeCalculationType.validValuesForShareAccountActivation());
|
||||||
}
|
}
|
||||||
|
} else if (appliesTo.isCurrentAccountCharge()) {
|
||||||
|
// current account applicable validation — same rules as savings
|
||||||
|
final Integer chargeTimeType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(CHARGE_TIME_TYPE, element);
|
||||||
|
baseDataValidator.reset().parameter(CHARGE_TIME_TYPE).value(chargeTimeType).notNull();
|
||||||
|
if (chargeTimeType != null) {
|
||||||
|
baseDataValidator.reset().parameter(CHARGE_TIME_TYPE).value(chargeTimeType)
|
||||||
|
.isOneOfTheseValues(ChargeTimeType.validCurrentAccountValues());
|
||||||
|
}
|
||||||
|
|
||||||
|
final ChargeTimeType ctt = ChargeTimeType.fromInt(chargeTimeType);
|
||||||
|
|
||||||
|
if (ctt.isWeeklyFee()) {
|
||||||
|
final String monthDay = this.fromApiJsonHelper.extractStringNamed(FEE_ON_MONTH_DAY, element);
|
||||||
|
baseDataValidator.reset().parameter(FEE_ON_MONTH_DAY).value(monthDay).mustBeBlankWhenParameterProvidedIs(CHARGE_TIME_TYPE,
|
||||||
|
chargeTimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctt.isMonthlyFee()) {
|
||||||
|
final MonthDay monthDay = this.fromApiJsonHelper.extractMonthDayNamed(FEE_ON_MONTH_DAY, element);
|
||||||
|
baseDataValidator.reset().parameter(FEE_ON_MONTH_DAY).value(monthDay).notNull();
|
||||||
|
baseDataValidator.reset().parameter(FEE_INTERVAL).value(feeInterval).notNull().inMinMaxRange(1, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctt.isAnnualFee()) {
|
||||||
|
final MonthDay monthDay = this.fromApiJsonHelper.extractMonthDayNamed(FEE_ON_MONTH_DAY, element);
|
||||||
|
baseDataValidator.reset().parameter(FEE_ON_MONTH_DAY).value(monthDay).notNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chargeCalculationType != null) {
|
||||||
|
baseDataValidator.reset().parameter(CHARGE_CALCULATION_TYPE).value(chargeCalculationType)
|
||||||
|
.isOneOfTheseValues(ChargeCalculationType.validValuesForCurrentAccount());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final String name = this.fromApiJsonHelper.extractStringNamed(NAME, element);
|
final String name = this.fromApiJsonHelper.extractStringNamed(NAME, element);
|
||||||
|
|||||||
+4
@@ -47,4 +47,8 @@ public interface ChargeDropdownReadPlatformService {
|
|||||||
|
|
||||||
List<EnumOptionData> retrieveSharesCollectionTimeTypes();
|
List<EnumOptionData> retrieveSharesCollectionTimeTypes();
|
||||||
|
|
||||||
|
List<EnumOptionData> retrieveCurrentAccountCalculationTypes();
|
||||||
|
|
||||||
|
List<EnumOptionData> retrieveCurrentAccountCollectionTimeTypes();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+14
@@ -124,4 +124,18 @@ public class ChargeDropdownReadPlatformServiceImpl implements ChargeDropdownRead
|
|||||||
return Arrays.asList(chargeTimeType(ChargeTimeType.SHAREACCOUNT_ACTIVATION), chargeTimeType(ChargeTimeType.SHARE_PURCHASE),
|
return Arrays.asList(chargeTimeType(ChargeTimeType.SHAREACCOUNT_ACTIVATION), chargeTimeType(ChargeTimeType.SHARE_PURCHASE),
|
||||||
chargeTimeType(ChargeTimeType.SHARE_REDEEM));
|
chargeTimeType(ChargeTimeType.SHARE_REDEEM));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<EnumOptionData> retrieveCurrentAccountCalculationTypes() {
|
||||||
|
return Arrays.asList(chargeCalculationType(ChargeCalculationType.FLAT),
|
||||||
|
chargeCalculationType(ChargeCalculationType.PERCENT_OF_AMOUNT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<EnumOptionData> retrieveCurrentAccountCollectionTimeTypes() {
|
||||||
|
return Arrays.asList(chargeTimeType(ChargeTimeType.SPECIFIED_DUE_DATE), chargeTimeType(ChargeTimeType.SAVINGS_ACTIVATION),
|
||||||
|
chargeTimeType(ChargeTimeType.WITHDRAWAL_FEE), chargeTimeType(ChargeTimeType.ANNUAL_FEE),
|
||||||
|
chargeTimeType(ChargeTimeType.MONTHLY_FEE), chargeTimeType(ChargeTimeType.WEEKLY_FEE),
|
||||||
|
chargeTimeType(ChargeTimeType.OVERDRAFT_FEE), chargeTimeType(ChargeTimeType.SAVINGS_NOACTIVITY_FEE));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -130,6 +130,10 @@ public final class ChargeEnumerations {
|
|||||||
case SHARES:
|
case SHARES:
|
||||||
optionData = new EnumOptionData(ChargeAppliesTo.SHARES.getValue().longValue(), ChargeAppliesTo.SHARES.getCode(), "Shares");
|
optionData = new EnumOptionData(ChargeAppliesTo.SHARES.getValue().longValue(), ChargeAppliesTo.SHARES.getCode(), "Shares");
|
||||||
break;
|
break;
|
||||||
|
case CURRENT_ACCOUNT:
|
||||||
|
optionData = new EnumOptionData(ChargeAppliesTo.CURRENT_ACCOUNT.getValue().longValue(),
|
||||||
|
ChargeAppliesTo.CURRENT_ACCOUNT.getCode(), "Current Account");
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
optionData = new EnumOptionData(ChargeAppliesTo.INVALID.getValue().longValue(), ChargeAppliesTo.INVALID.getCode(),
|
optionData = new EnumOptionData(ChargeAppliesTo.INVALID.getValue().longValue(), ChargeAppliesTo.INVALID.getCode(),
|
||||||
"Invalid");
|
"Invalid");
|
||||||
|
|||||||
+1
@@ -60,6 +60,7 @@ public enum JobName {
|
|||||||
ACCRUAL_ACTIVITY_POSTING("Accrual Activity Posting"), //
|
ACCRUAL_ACTIVITY_POSTING("Accrual Activity Posting"), //
|
||||||
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"), //
|
||||||
; //
|
; //
|
||||||
|
|
||||||
private final String name;
|
private final String name;
|
||||||
|
|||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.infrastructure.openproject.data;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Accessors(chain = true)
|
||||||
|
public final class OpenProjectProjectData {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String identifier;
|
||||||
|
private String name;
|
||||||
|
private boolean active;
|
||||||
|
private String description;
|
||||||
|
private String createdAt;
|
||||||
|
private String updatedAt;
|
||||||
|
}
|
||||||
+35
@@ -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.infrastructure.openproject.data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Accessors(chain = true)
|
||||||
|
public final class OpenProjectStatusSummaryData {
|
||||||
|
|
||||||
|
private int totalWorkPackages;
|
||||||
|
private Map<String, Integer> byStatus;
|
||||||
|
private Map<String, Integer> byType;
|
||||||
|
private Map<String, Integer> byPriority;
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.infrastructure.openproject.data;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Accessors(chain = true)
|
||||||
|
public final class OpenProjectWorkPackageData {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String subject;
|
||||||
|
private String description;
|
||||||
|
private String typeName;
|
||||||
|
private String statusName;
|
||||||
|
private String priorityName;
|
||||||
|
private String assigneeName;
|
||||||
|
private Integer percentageDone;
|
||||||
|
private String startDate;
|
||||||
|
private String dueDate;
|
||||||
|
private String createdAt;
|
||||||
|
private String updatedAt;
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.infrastructure.rbifeed.data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Accessors(chain = true)
|
||||||
|
public final class RbiFeedItemData implements Serializable {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String title;
|
||||||
|
private String description;
|
||||||
|
private String link;
|
||||||
|
private LocalDateTime pubDate;
|
||||||
|
private String category;
|
||||||
|
private LocalDateTime fetchedOn;
|
||||||
|
private Long openProjectWpId;
|
||||||
|
private String openProjectWpStatus;
|
||||||
|
private Integer openProjectWpProgress;
|
||||||
|
|
||||||
|
public static RbiFeedItemData instance(final Long id, final String title, final String description, final String link,
|
||||||
|
final LocalDateTime pubDate, final String category, final LocalDateTime fetchedOn,
|
||||||
|
final Long openProjectWpId, final String openProjectWpStatus, final Integer openProjectWpProgress) {
|
||||||
|
return new RbiFeedItemData().setId(id).setTitle(title).setDescription(description).setLink(link).setPubDate(pubDate)
|
||||||
|
.setCategory(category).setFetchedOn(fetchedOn)
|
||||||
|
.setOpenProjectWpId(openProjectWpId).setOpenProjectWpStatus(openProjectWpStatus)
|
||||||
|
.setOpenProjectWpProgress(openProjectWpProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.rbifeed.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.persistence.UniqueConstraint;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_rbi_feed_item", uniqueConstraints = {
|
||||||
|
@UniqueConstraint(columnNames = { "link" }, name = "uk_rbi_feed_item_link") })
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Accessors(chain = true)
|
||||||
|
public class RbiFeedItem extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
@Column(name = "title", nullable = false, length = 1000)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Column(name = "description", columnDefinition = "TEXT")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "link", nullable = false, length = 1000)
|
||||||
|
private String link;
|
||||||
|
|
||||||
|
@Column(name = "pub_date")
|
||||||
|
private LocalDateTime pubDate;
|
||||||
|
|
||||||
|
@Column(name = "category", nullable = false, length = 50)
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Column(name = "fetched_on")
|
||||||
|
private LocalDateTime fetchedOn;
|
||||||
|
|
||||||
|
@Column(name = "openproject_wp_id")
|
||||||
|
private Long openProjectWpId;
|
||||||
|
|
||||||
|
@Column(name = "openproject_wp_status", length = 100)
|
||||||
|
private String openProjectWpStatus;
|
||||||
|
|
||||||
|
@Column(name = "openproject_wp_progress")
|
||||||
|
private Integer openProjectWpProgress;
|
||||||
|
}
|
||||||
+36
@@ -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.infrastructure.rbifeed.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
|
public interface RbiFeedItemRepository extends JpaRepository<RbiFeedItem, Long>, JpaSpecificationExecutor<RbiFeedItem> {
|
||||||
|
|
||||||
|
boolean existsByLink(String link);
|
||||||
|
|
||||||
|
List<RbiFeedItem> findByCategoryOrderByPubDateDesc(String category);
|
||||||
|
|
||||||
|
List<RbiFeedItem> findAllByOrderByPubDateDesc();
|
||||||
|
|
||||||
|
List<RbiFeedItem> findByCategoryAndOpenProjectWpIdIsNotNull(String category);
|
||||||
|
|
||||||
|
List<RbiFeedItem> findByCategoryAndOpenProjectWpIdIsNull(String category);
|
||||||
|
}
|
||||||
@@ -87,6 +87,8 @@ public final class ChargeData implements Comparable<ChargeData>, Serializable {
|
|||||||
private final List<EnumOptionData> clientChargeTimeTypeOptions;
|
private final List<EnumOptionData> clientChargeTimeTypeOptions;
|
||||||
private final List<EnumOptionData> shareChargeCalculationTypeOptions;
|
private final List<EnumOptionData> shareChargeCalculationTypeOptions;
|
||||||
private final List<EnumOptionData> shareChargeTimeTypeOptions;
|
private final List<EnumOptionData> shareChargeTimeTypeOptions;
|
||||||
|
private final List<EnumOptionData> currentAccountChargeCalculationTypeOptions;
|
||||||
|
private final List<EnumOptionData> currentAccountChargeTimeTypeOptions;
|
||||||
|
|
||||||
private final List<EnumOptionData> feeFrequencyOptions;
|
private final List<EnumOptionData> feeFrequencyOptions;
|
||||||
|
|
||||||
@@ -113,6 +115,8 @@ public final class ChargeData implements Comparable<ChargeData>, Serializable {
|
|||||||
.taxGroupOptions(template.getTaxGroupOptions())
|
.taxGroupOptions(template.getTaxGroupOptions())
|
||||||
.shareChargeCalculationTypeOptions(template.getShareChargeCalculationTypeOptions())
|
.shareChargeCalculationTypeOptions(template.getShareChargeCalculationTypeOptions())
|
||||||
.shareChargeTimeTypeOptions(template.getShareChargeTimeTypeOptions())
|
.shareChargeTimeTypeOptions(template.getShareChargeTimeTypeOptions())
|
||||||
|
.currentAccountChargeCalculationTypeOptions(template.getCurrentAccountChargeCalculationTypeOptions())
|
||||||
|
.currentAccountChargeTimeTypeOptions(template.getCurrentAccountChargeTimeTypeOptions())
|
||||||
.accountMappingForChargeConfig(template.getAccountMappingForChargeConfig())
|
.accountMappingForChargeConfig(template.getAccountMappingForChargeConfig())
|
||||||
.expenseAccountOptions(template.getExpenseAccountOptions()).assetAccountOptions(template.getAssetAccountOptions()).build();
|
.expenseAccountOptions(template.getExpenseAccountOptions()).assetAccountOptions(template.getAssetAccountOptions()).build();
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -72,6 +72,13 @@ public enum ChargeTimeType {
|
|||||||
ChargeTimeType.SAVINGS_NOACTIVITY_FEE.getValue() };
|
ChargeTimeType.SAVINGS_NOACTIVITY_FEE.getValue() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Object[] validCurrentAccountValues() {
|
||||||
|
return new Integer[] { ChargeTimeType.SPECIFIED_DUE_DATE.getValue(), ChargeTimeType.SAVINGS_ACTIVATION.getValue(),
|
||||||
|
ChargeTimeType.SAVINGS_CLOSURE.getValue(), ChargeTimeType.WITHDRAWAL_FEE.getValue(), ChargeTimeType.ANNUAL_FEE.getValue(),
|
||||||
|
ChargeTimeType.MONTHLY_FEE.getValue(), ChargeTimeType.OVERDRAFT_FEE.getValue(), ChargeTimeType.WEEKLY_FEE.getValue(),
|
||||||
|
ChargeTimeType.SAVINGS_NOACTIVITY_FEE.getValue() };
|
||||||
|
}
|
||||||
|
|
||||||
public static Object[] validClientValues() {
|
public static Object[] validClientValues() {
|
||||||
return new Integer[] { ChargeTimeType.SPECIFIED_DUE_DATE.getValue() };
|
return new Integer[] { ChargeTimeType.SPECIFIED_DUE_DATE.getValue() };
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -181,6 +181,7 @@ public class ClientApiConstants {
|
|||||||
|
|
||||||
public static final String datatables = "datatables";
|
public static final String datatables = "datatables";
|
||||||
public static final String obligeeData = "ObligeeDetails";
|
public static final String obligeeData = "ObligeeDetails";
|
||||||
|
public static final String kycParamName = "kyc";
|
||||||
|
|
||||||
public static final String clientEntityName = "clients";
|
public static final String clientEntityName = "clients";
|
||||||
|
|
||||||
|
|||||||
+4
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.client.data;
|
|||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -53,4 +54,7 @@ public class ClientFamilyMemberRequest implements Serializable {
|
|||||||
private Long id;
|
private Long id;
|
||||||
private Long age;
|
private Long age;
|
||||||
private Long professionId;
|
private Long professionId;
|
||||||
|
private String nominationType;
|
||||||
|
private BigDecimal percentageShare;
|
||||||
|
private String remarks;
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -20,6 +20,7 @@
|
|||||||
package org.apache.fineract.portfolio.client.data;
|
package org.apache.fineract.portfolio.client.data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
@@ -66,6 +67,12 @@ public final class ClientFamilyMembersData implements Serializable {
|
|||||||
|
|
||||||
private final Boolean isDependent;
|
private final Boolean isDependent;
|
||||||
|
|
||||||
|
private final String nominationType;
|
||||||
|
|
||||||
|
private final BigDecimal percentageShare;
|
||||||
|
|
||||||
|
private final String remarks;
|
||||||
|
|
||||||
// template holder
|
// template holder
|
||||||
private final Collection<CodeValueData> relationshipIdOptions;
|
private final Collection<CodeValueData> relationshipIdOptions;
|
||||||
private final Collection<CodeValueData> genderIdOptions;
|
private final Collection<CodeValueData> genderIdOptions;
|
||||||
|
|||||||
+380
@@ -0,0 +1,380 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* 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.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.persistence.UniqueConstraint;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a client's KYC (Know Your Customer) record as mandated by the Reserve Bank of India
|
||||||
|
* Master Direction on KYC (updated 2023).
|
||||||
|
*
|
||||||
|
* <p>One record per client. Contains:
|
||||||
|
* <ul>
|
||||||
|
* <li>PAN verification (Permanent Account Number – Income Tax Dept)</li>
|
||||||
|
* <li>Aadhaar verification (12-digit UIDAI number, stored masked)</li>
|
||||||
|
* <li>GSTIN verification (for non-individual/business clients)</li>
|
||||||
|
* <li>Risk category, CKYCRR number, expiry & lifecycle timestamps</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_client_kyc", uniqueConstraints = {
|
||||||
|
@UniqueConstraint(columnNames = { "client_id" }, name = "uq_m_client_kyc_client_id") })
|
||||||
|
public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "client_id", nullable = false)
|
||||||
|
private Client client;
|
||||||
|
|
||||||
|
/** 100 = PENDING, 200 = IN_PROGRESS, 300 = VERIFIED, 400 = REJECTED, 500 = EXPIRED */
|
||||||
|
@Column(name = "kyc_status", nullable = false)
|
||||||
|
private Integer kycStatus;
|
||||||
|
|
||||||
|
/** 1 = INDIVIDUAL, 2 = NON_INDIVIDUAL */
|
||||||
|
@Column(name = "kyc_type", nullable = false)
|
||||||
|
private Integer kycType;
|
||||||
|
|
||||||
|
/** 1 = LOW, 2 = MEDIUM, 3 = HIGH */
|
||||||
|
@Column(name = "risk_category", nullable = false)
|
||||||
|
private Integer riskCategory;
|
||||||
|
|
||||||
|
// ── PAN (Permanent Account Number) ──────────────────────────────────────
|
||||||
|
@Column(name = "pan_number", length = 10)
|
||||||
|
private String panNumber;
|
||||||
|
|
||||||
|
@Column(name = "pan_verified", nullable = false)
|
||||||
|
private boolean panVerified;
|
||||||
|
|
||||||
|
@Column(name = "pan_verified_on")
|
||||||
|
private LocalDate panVerifiedOn;
|
||||||
|
|
||||||
|
@Column(name = "pan_full_name", length = 200)
|
||||||
|
private String panFullName;
|
||||||
|
|
||||||
|
/** Y = YES, N = NO, NA = Not Available (Aadhaar seeding status on PAN card) */
|
||||||
|
@Column(name = "pan_aadhaar_seeding_status", length = 5)
|
||||||
|
private String panAadhaarSeedingStatus;
|
||||||
|
|
||||||
|
/** PAN card category: individual, company, firm, etc. */
|
||||||
|
@Column(name = "pan_category", length = 50)
|
||||||
|
private String panCategory;
|
||||||
|
|
||||||
|
/** PAN validation status: valid or invalid */
|
||||||
|
@Column(name = "pan_status", length = 20)
|
||||||
|
private String panStatus;
|
||||||
|
|
||||||
|
// ── Aadhaar ──────────────────────────────────────────────────────────────
|
||||||
|
/** Stored masked (last 4 digits visible). Full number must never be persisted as per UIDAI directive. */
|
||||||
|
@Column(name = "aadhaar_number_masked", length = 12)
|
||||||
|
private String aadhaarNumberMasked;
|
||||||
|
|
||||||
|
@Column(name = "aadhaar_verified", nullable = false)
|
||||||
|
private boolean aadhaarVerified;
|
||||||
|
|
||||||
|
@Column(name = "aadhaar_verified_on")
|
||||||
|
private LocalDate aadhaarVerifiedOn;
|
||||||
|
|
||||||
|
// ── GSTIN (mandatory for NON_INDIVIDUAL clients) ─────────────────────────
|
||||||
|
@Column(name = "gstin_number", length = 15)
|
||||||
|
private String gstinNumber;
|
||||||
|
|
||||||
|
@Column(name = "gstin_verified", nullable = false)
|
||||||
|
private boolean gstinVerified;
|
||||||
|
|
||||||
|
@Column(name = "gstin_verified_on")
|
||||||
|
private LocalDate gstinVerifiedOn;
|
||||||
|
|
||||||
|
// ── CKYCRR ───────────────────────────────────────────────────────────────
|
||||||
|
/** Central KYC Records Registry number issued by CERSAI */
|
||||||
|
@Column(name = "ckyc_number", length = 14)
|
||||||
|
private String ckycNumber;
|
||||||
|
|
||||||
|
// ── Lifecycle dates ───────────────────────────────────────────────────────
|
||||||
|
@Column(name = "initiated_on")
|
||||||
|
private LocalDate initiatedOn;
|
||||||
|
|
||||||
|
@Column(name = "verified_on")
|
||||||
|
private LocalDate verifiedOn;
|
||||||
|
|
||||||
|
@Column(name = "rejected_on")
|
||||||
|
private LocalDate rejectedOn;
|
||||||
|
|
||||||
|
/** KYC expiry date calculated based on risk category and verification date */
|
||||||
|
@Column(name = "expiry_date")
|
||||||
|
private LocalDate expiryDate;
|
||||||
|
|
||||||
|
@Column(name = "remarks", length = 500)
|
||||||
|
private String remarks;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
protected ClientKyc() {}
|
||||||
|
|
||||||
|
private ClientKyc(final Client client, final Integer kycType, final Integer riskCategory,
|
||||||
|
final String panNumber, final String aadhaarNumberMasked, final String gstinNumber,
|
||||||
|
final String remarks) {
|
||||||
|
this.client = client;
|
||||||
|
this.kycStatus = ClientKycStatus.PENDING.getValue();
|
||||||
|
this.kycType = kycType;
|
||||||
|
this.riskCategory = riskCategory;
|
||||||
|
this.panNumber = StringUtils.defaultIfEmpty(panNumber, null);
|
||||||
|
this.aadhaarNumberMasked = StringUtils.defaultIfEmpty(aadhaarNumberMasked, null);
|
||||||
|
this.gstinNumber = StringUtils.defaultIfEmpty(gstinNumber, null);
|
||||||
|
this.remarks = StringUtils.defaultIfEmpty(remarks, null);
|
||||||
|
this.panVerified = false;
|
||||||
|
this.aadhaarVerified = false;
|
||||||
|
this.gstinVerified = false;
|
||||||
|
this.initiatedOn = LocalDate.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ClientKyc initiateKyc(final Client client, final JsonCommand command) {
|
||||||
|
final Integer kycType = command.integerValueOfParameterNamed("kycType");
|
||||||
|
final Integer riskCategory = command.integerValueOfParameterNamed("riskCategory");
|
||||||
|
final String panNumber = command.stringValueOfParameterNamed("panNumber");
|
||||||
|
final String aadhaarNumberMasked = command.stringValueOfParameterNamed("aadhaarNumberMasked");
|
||||||
|
final String gstinNumber = command.stringValueOfParameterNamed("gstinNumber");
|
||||||
|
final String remarks = command.stringValueOfParameterNamed("remarks");
|
||||||
|
return new ClientKyc(client, kycType, riskCategory, panNumber, aadhaarNumberMasked, gstinNumber, remarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method used when KYC is initiated inline during client registration.
|
||||||
|
* Accepts individual field values directly (avoids needing a JsonCommand sub-object).
|
||||||
|
*/
|
||||||
|
public static ClientKyc initiateKyc(final Client client, final Integer kycType, final Integer riskCategory,
|
||||||
|
final String panNumber, final String aadhaarNumberMasked, final String gstinNumber, final String remarks) {
|
||||||
|
return new ClientKyc(client, kycType, riskCategory, panNumber, aadhaarNumberMasked, gstinNumber, remarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> update(final JsonCommand command) {
|
||||||
|
final Map<String, Object> actualChanges = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
final String panNumberParam = "panNumber";
|
||||||
|
if (command.isChangeInStringParameterNamed(panNumberParam, this.panNumber)) {
|
||||||
|
final String newValue = command.stringValueOfParameterNamed(panNumberParam);
|
||||||
|
actualChanges.put(panNumberParam, newValue);
|
||||||
|
this.panNumber = StringUtils.defaultIfEmpty(newValue, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String aadhaarParam = "aadhaarNumberMasked";
|
||||||
|
if (command.isChangeInStringParameterNamed(aadhaarParam, this.aadhaarNumberMasked)) {
|
||||||
|
final String newValue = command.stringValueOfParameterNamed(aadhaarParam);
|
||||||
|
actualChanges.put(aadhaarParam, newValue);
|
||||||
|
this.aadhaarNumberMasked = StringUtils.defaultIfEmpty(newValue, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String gstinParam = "gstinNumber";
|
||||||
|
if (command.isChangeInStringParameterNamed(gstinParam, this.gstinNumber)) {
|
||||||
|
final String newValue = command.stringValueOfParameterNamed(gstinParam);
|
||||||
|
actualChanges.put(gstinParam, newValue);
|
||||||
|
this.gstinNumber = StringUtils.defaultIfEmpty(newValue, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String riskParam = "riskCategory";
|
||||||
|
if (command.isChangeInIntegerParameterNamed(riskParam, this.riskCategory)) {
|
||||||
|
final Integer newValue = command.integerValueOfParameterNamed(riskParam);
|
||||||
|
actualChanges.put(riskParam, newValue);
|
||||||
|
this.riskCategory = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String remarksParam = "remarks";
|
||||||
|
if (command.isChangeInStringParameterNamed(remarksParam, this.remarks)) {
|
||||||
|
final String newValue = command.stringValueOfParameterNamed(remarksParam);
|
||||||
|
actualChanges.put(remarksParam, newValue);
|
||||||
|
this.remarks = StringUtils.defaultIfEmpty(newValue, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return actualChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called when documents are submitted and 3rd-party verification is initiated. */
|
||||||
|
public void markInProgress() {
|
||||||
|
this.kycStatus = ClientKycStatus.IN_PROGRESS.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called after successful 3rd-party verification of all required documents. */
|
||||||
|
public void markVerified(final String ckycNumber) {
|
||||||
|
this.kycStatus = ClientKycStatus.VERIFIED.getValue();
|
||||||
|
this.verifiedOn = LocalDate.now();
|
||||||
|
this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null);
|
||||||
|
|
||||||
|
final KycRiskCategory risk = KycRiskCategory.fromInt(this.riskCategory);
|
||||||
|
if (risk.getReKycYears() > 0) {
|
||||||
|
this.expiryDate = this.verifiedOn.plusYears(risk.getReKycYears());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called when verification fails. */
|
||||||
|
public void markRejected(final String remarks) {
|
||||||
|
this.kycStatus = ClientKycStatus.REJECTED.getValue();
|
||||||
|
this.rejectedOn = LocalDate.now();
|
||||||
|
this.remarks = StringUtils.defaultIfEmpty(remarks, this.remarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markPanVerified(final String fullName, final String aadhaarSeedingStatus,
|
||||||
|
final String category, final String status) {
|
||||||
|
this.panVerified = true;
|
||||||
|
this.panVerifiedOn = LocalDate.now();
|
||||||
|
this.panFullName = StringUtils.defaultIfEmpty(fullName, null);
|
||||||
|
this.panAadhaarSeedingStatus = normalizeAadhaarSeedingStatus(aadhaarSeedingStatus);
|
||||||
|
this.panCategory = StringUtils.defaultIfEmpty(category, null);
|
||||||
|
this.panStatus = StringUtils.defaultIfEmpty(status, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Normalize aadhaar seeding status: y→Y, n→N, anything else→NA */
|
||||||
|
private String normalizeAadhaarSeedingStatus(final String raw) {
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
return "NA";
|
||||||
|
}
|
||||||
|
final String trimmed = raw.trim().toUpperCase();
|
||||||
|
if ("Y".equals(trimmed) || "YES".equals(trimmed)) {
|
||||||
|
return "Y";
|
||||||
|
}
|
||||||
|
if ("N".equals(trimmed) || "NO".equals(trimmed)) {
|
||||||
|
return "N";
|
||||||
|
}
|
||||||
|
return "NA";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markAadhaarVerified() {
|
||||||
|
this.aadhaarVerified = true;
|
||||||
|
this.aadhaarVerifiedOn = LocalDate.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markGstinVerified() {
|
||||||
|
this.gstinVerified = true;
|
||||||
|
this.gstinVerifiedOn = LocalDate.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Accessors ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public Client getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientKycStatus getKycStatus() {
|
||||||
|
return ClientKycStatus.fromInt(this.kycStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getKycStatusValue() {
|
||||||
|
return kycStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientKycType getKycType() {
|
||||||
|
return ClientKycType.fromInt(this.kycType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getKycTypeValue() {
|
||||||
|
return kycType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KycRiskCategory getRiskCategory() {
|
||||||
|
return KycRiskCategory.fromInt(this.riskCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getRiskCategoryValue() {
|
||||||
|
return riskCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPanNumber() {
|
||||||
|
return panNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPanVerified() {
|
||||||
|
return panVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getPanVerifiedOn() {
|
||||||
|
return panVerifiedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPanFullName() {
|
||||||
|
return panFullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPanAadhaarSeedingStatus() {
|
||||||
|
return panAadhaarSeedingStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPanCategory() {
|
||||||
|
return panCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPanStatus() {
|
||||||
|
return panStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAadhaarNumberMasked() {
|
||||||
|
return aadhaarNumberMasked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAadhaarVerified() {
|
||||||
|
return aadhaarVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getAadhaarVerifiedOn() {
|
||||||
|
return aadhaarVerifiedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGstinNumber() {
|
||||||
|
return gstinNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGstinVerified() {
|
||||||
|
return gstinVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getGstinVerifiedOn() {
|
||||||
|
return gstinVerifiedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCkycNumber() {
|
||||||
|
return ckycNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getInitiatedOn() {
|
||||||
|
return initiatedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getVerifiedOn() {
|
||||||
|
return verifiedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getRejectedOn() {
|
||||||
|
return rejectedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getExpiryDate() {
|
||||||
|
return expiryDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRemarks() {
|
||||||
|
return remarks;
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
@@ -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.client.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
public interface ClientKycRepository extends JpaRepository<ClientKyc, Long> {
|
||||||
|
|
||||||
|
Optional<ClientKyc> findByClientId(Long clientId);
|
||||||
|
|
||||||
|
@Query("SELECT k FROM ClientKyc k WHERE k.client.id = :clientId")
|
||||||
|
Optional<ClientKyc> findKycByClientId(@Param("clientId") Long clientId);
|
||||||
|
|
||||||
|
boolean existsByClientId(Long clientId);
|
||||||
|
|
||||||
|
@Query("SELECT k FROM ClientKyc k WHERE k.kycStatus = :status")
|
||||||
|
List<ClientKyc> findAllByStatus(@Param("status") Integer status);
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.domain;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps {@link ClientKycRepository} to surface consistent not-found exceptions.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ClientKycRepositoryWrapper {
|
||||||
|
|
||||||
|
private final ClientKycRepository repository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public ClientKyc findOneWithNotFoundDetection(final Long id) {
|
||||||
|
return repository.findById(id).orElseThrow(
|
||||||
|
() -> new org.apache.fineract.portfolio.client.exception.ClientKycNotFoundException(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public ClientKyc findByClientIdWithNotFoundDetection(final Long clientId) {
|
||||||
|
return repository.findByClientId(clientId).orElseThrow(
|
||||||
|
() -> new org.apache.fineract.portfolio.client.exception.ClientKycNotFoundException(clientId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientKyc saveAndFlush(final ClientKyc kyc) {
|
||||||
|
return repository.saveAndFlush(kyc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(final ClientKyc kyc) {
|
||||||
|
repository.delete(kyc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean existsByClientId(final Long clientId) {
|
||||||
|
return repository.existsByClientId(clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.domain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KYC status lifecycle as per RBI Master Direction on KYC (updated 2023).
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* PENDING -> IN_PROGRESS (documents submitted)
|
||||||
|
* IN_PROGRESS -> VERIFIED (3rd-party check passed)
|
||||||
|
* IN_PROGRESS -> REJECTED (3rd-party check failed / fraud detected)
|
||||||
|
* VERIFIED -> EXPIRED (re-KYC due date crossed)
|
||||||
|
* EXPIRED -> IN_PROGRESS (re-KYC initiated)
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public enum ClientKycStatus {
|
||||||
|
|
||||||
|
INVALID(0, "clientKycStatus.invalid"),
|
||||||
|
PENDING(100, "clientKycStatus.pending"),
|
||||||
|
IN_PROGRESS(200, "clientKycStatus.inProgress"),
|
||||||
|
VERIFIED(300, "clientKycStatus.verified"),
|
||||||
|
REJECTED(400, "clientKycStatus.rejected"),
|
||||||
|
EXPIRED(500, "clientKycStatus.expired");
|
||||||
|
|
||||||
|
private final Integer value;
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
ClientKycStatus(final Integer value, final String code) {
|
||||||
|
this.value = value;
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPending() {
|
||||||
|
return this == PENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInProgress() {
|
||||||
|
return this == IN_PROGRESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isVerified() {
|
||||||
|
return this == VERIFIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRejected() {
|
||||||
|
return this == REJECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExpired() {
|
||||||
|
return this == EXPIRED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ClientKycStatus fromInt(final Integer value) {
|
||||||
|
if (value == null) {
|
||||||
|
return INVALID;
|
||||||
|
}
|
||||||
|
for (ClientKycStatus status : values()) {
|
||||||
|
if (status.getValue().equals(value)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return INVALID;
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KYC entity type as defined under RBI Master Direction on KYC.
|
||||||
|
* INDIVIDUAL – natural persons.
|
||||||
|
* NON_INDIVIDUAL – companies, trusts, partnerships, HUFs, etc.
|
||||||
|
*/
|
||||||
|
public enum ClientKycType {
|
||||||
|
|
||||||
|
INVALID(0, "clientKycType.invalid"),
|
||||||
|
INDIVIDUAL(1, "clientKycType.individual"),
|
||||||
|
NON_INDIVIDUAL(2, "clientKycType.nonIndividual");
|
||||||
|
|
||||||
|
private final Integer value;
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
ClientKycType(final Integer value, final String code) {
|
||||||
|
this.value = value;
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isIndividual() {
|
||||||
|
return this == INDIVIDUAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNonIndividual() {
|
||||||
|
return this == NON_INDIVIDUAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ClientKycType fromInt(final Integer value) {
|
||||||
|
if (value == null) {
|
||||||
|
return INVALID;
|
||||||
|
}
|
||||||
|
for (ClientKycType type : values()) {
|
||||||
|
if (type.getValue().equals(value)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return INVALID;
|
||||||
|
}
|
||||||
|
}
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.domain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customer risk category used for periodic re-KYC frequency as per RBI guidelines.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>LOW – re-KYC every 10 years</li>
|
||||||
|
* <li>MEDIUM – re-KYC every 8 years</li>
|
||||||
|
* <li>HIGH – re-KYC every 2 years</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public enum KycRiskCategory {
|
||||||
|
|
||||||
|
INVALID(0, "kycRiskCategory.invalid", 0),
|
||||||
|
LOW(1, "kycRiskCategory.low", 10),
|
||||||
|
MEDIUM(2, "kycRiskCategory.medium", 8),
|
||||||
|
HIGH(3, "kycRiskCategory.high", 2);
|
||||||
|
|
||||||
|
private final Integer value;
|
||||||
|
private final String code;
|
||||||
|
/** Re-KYC validity in years */
|
||||||
|
private final int reKycYears;
|
||||||
|
|
||||||
|
KycRiskCategory(final Integer value, final String code, final int reKycYears) {
|
||||||
|
this.value = value;
|
||||||
|
this.code = code;
|
||||||
|
this.reKycYears = reKycYears;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getReKycYears() {
|
||||||
|
return reKycYears;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static KycRiskCategory fromInt(final Integer value) {
|
||||||
|
if (value == null) {
|
||||||
|
return INVALID;
|
||||||
|
}
|
||||||
|
for (KycRiskCategory cat : values()) {
|
||||||
|
if (cat.getValue().equals(value)) {
|
||||||
|
return cat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return INVALID;
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||||
|
|
||||||
|
public class ClientKycAlreadyExistsException extends AbstractPlatformDomainRuleException {
|
||||||
|
|
||||||
|
public ClientKycAlreadyExistsException(final Long clientId) {
|
||||||
|
super("error.msg.client.kyc.already.exists",
|
||||||
|
"A KYC record already exists for client with identifier " + clientId
|
||||||
|
+ ". Use the update endpoint to modify it.",
|
||||||
|
clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -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.client.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||||
|
|
||||||
|
public class ClientKycInvalidStateTransitionException extends AbstractPlatformDomainRuleException {
|
||||||
|
|
||||||
|
public ClientKycInvalidStateTransitionException(final String action, final String currentState) {
|
||||||
|
super("error.msg.client.kyc.invalid.state.transition",
|
||||||
|
"KYC action [" + action + "] is not permitted when the KYC status is [" + currentState + "]",
|
||||||
|
action, currentState);
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -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.client.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||||
|
|
||||||
|
public class ClientKycNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||||
|
|
||||||
|
public ClientKycNotFoundException(final Long id) {
|
||||||
|
super("error.msg.client.kyc.not.found", "Client KYC record with identifier " + id + " does not exist", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientKycNotFoundException(final Long clientId, final boolean byClient) {
|
||||||
|
super("error.msg.client.kyc.not.found",
|
||||||
|
"No KYC record found for client with identifier " + clientId, clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.mobilelookup.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity log for each DIP / MNRL mobile number lookup request.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_mobile_lookup_log")
|
||||||
|
@Getter
|
||||||
|
public class MobileLookupLog extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@Column(name = "mobile_number", nullable = false, length = 15)
|
||||||
|
private String mobileNumber;
|
||||||
|
|
||||||
|
/** CLEAN, SUSPECTED, DISCONNECTED, SUSPECTED_AND_DISCONNECTED, ERROR */
|
||||||
|
@Column(name = "lookup_status", nullable = false, length = 50)
|
||||||
|
private String lookupStatus;
|
||||||
|
|
||||||
|
@Column(name = "suspected", nullable = false)
|
||||||
|
private boolean suspected;
|
||||||
|
|
||||||
|
@Column(name = "disconnected", nullable = false)
|
||||||
|
private boolean disconnected;
|
||||||
|
|
||||||
|
@Column(name = "response_message", length = 500)
|
||||||
|
private String responseMessage;
|
||||||
|
|
||||||
|
/** Full JSON response from the external API (for audit trail) */
|
||||||
|
@Column(name = "raw_response", columnDefinition = "TEXT")
|
||||||
|
private String rawResponse;
|
||||||
|
|
||||||
|
/** CLIENT_REGISTRATION, MANUAL_CHECK, API */
|
||||||
|
@Column(name = "lookup_source", length = 50)
|
||||||
|
private String lookupSource;
|
||||||
|
|
||||||
|
@Column(name = "client_id")
|
||||||
|
private Long clientId;
|
||||||
|
|
||||||
|
@Column(name = "looked_up_on", nullable = false)
|
||||||
|
private OffsetDateTime lookedUpOn;
|
||||||
|
|
||||||
|
protected MobileLookupLog() {}
|
||||||
|
|
||||||
|
public MobileLookupLog(final String mobileNumber, final String lookupStatus,
|
||||||
|
final boolean suspected, final boolean disconnected,
|
||||||
|
final String responseMessage, final String rawResponse,
|
||||||
|
final String lookupSource, final Long clientId) {
|
||||||
|
this.mobileNumber = mobileNumber;
|
||||||
|
this.lookupStatus = lookupStatus;
|
||||||
|
this.suspected = suspected;
|
||||||
|
this.disconnected = disconnected;
|
||||||
|
this.responseMessage = responseMessage;
|
||||||
|
this.rawResponse = rawResponse;
|
||||||
|
this.lookupSource = lookupSource;
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.lookedUpOn = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
@@ -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.mobilelookup.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;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface MobileLookupLogRepository extends JpaRepository<MobileLookupLog, Long> {
|
||||||
|
|
||||||
|
@Query("SELECT l FROM MobileLookupLog l ORDER BY l.lookedUpOn DESC")
|
||||||
|
List<MobileLookupLog> findAllOrderByLookedUpOnDesc();
|
||||||
|
|
||||||
|
@Query("SELECT l FROM MobileLookupLog l WHERE l.mobileNumber = :mobileNumber ORDER BY l.lookedUpOn DESC")
|
||||||
|
List<MobileLookupLog> findByMobileNumber(@Param("mobileNumber") String mobileNumber);
|
||||||
|
|
||||||
|
@Query("SELECT l FROM MobileLookupLog l WHERE l.lookupStatus = :status ORDER BY l.lookedUpOn DESC")
|
||||||
|
List<MobileLookupLog> findByLookupStatus(@Param("status") String status);
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.sanctions.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an alias (also-known-as) name for a sanctions list entry.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_sanctions_alias")
|
||||||
|
public class SanctionsAlias extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "sanctions_entry_id", nullable = false)
|
||||||
|
private SanctionsEntry sanctionsEntry;
|
||||||
|
|
||||||
|
@Column(name = "alias_quality", length = 50)
|
||||||
|
private String aliasQuality;
|
||||||
|
|
||||||
|
@Column(name = "alias_name", nullable = false, length = 500)
|
||||||
|
private String aliasName;
|
||||||
|
|
||||||
|
protected SanctionsAlias() {}
|
||||||
|
|
||||||
|
public SanctionsAlias(final String aliasQuality, final String aliasName) {
|
||||||
|
this.aliasQuality = aliasQuality;
|
||||||
|
this.aliasName = aliasName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSanctionsEntry(SanctionsEntry sanctionsEntry) {
|
||||||
|
this.sanctionsEntry = sanctionsEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SanctionsEntry getSanctionsEntry() { return sanctionsEntry; }
|
||||||
|
public String getAliasQuality() { return aliasQuality; }
|
||||||
|
public String getAliasName() { return aliasName; }
|
||||||
|
}
|
||||||
+157
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.sanctions.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.CascadeType;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.OneToMany;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an individual or entity from the UN Security Council consolidated sanctions list.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_sanctions_entry")
|
||||||
|
public class SanctionsEntry extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@Column(name = "data_id", nullable = false, length = 50)
|
||||||
|
private String dataId;
|
||||||
|
|
||||||
|
/** INDIVIDUAL or ENTITY */
|
||||||
|
@Column(name = "entry_type", nullable = false, length = 20)
|
||||||
|
private String entryType;
|
||||||
|
|
||||||
|
@Column(name = "first_name", length = 255)
|
||||||
|
private String firstName;
|
||||||
|
|
||||||
|
@Column(name = "second_name", length = 255)
|
||||||
|
private String secondName;
|
||||||
|
|
||||||
|
@Column(name = "third_name", length = 255)
|
||||||
|
private String thirdName;
|
||||||
|
|
||||||
|
@Column(name = "fourth_name", length = 255)
|
||||||
|
private String fourthName;
|
||||||
|
|
||||||
|
@Column(name = "full_name", length = 500)
|
||||||
|
private String fullName;
|
||||||
|
|
||||||
|
@Column(name = "un_list_type", length = 255)
|
||||||
|
private String unListType;
|
||||||
|
|
||||||
|
@Column(name = "reference_number", length = 255)
|
||||||
|
private String referenceNumber;
|
||||||
|
|
||||||
|
@Column(name = "listed_on")
|
||||||
|
private LocalDate listedOn;
|
||||||
|
|
||||||
|
@Column(name = "gender", length = 20)
|
||||||
|
private String gender;
|
||||||
|
|
||||||
|
@Column(name = "nationality", length = 500)
|
||||||
|
private String nationality;
|
||||||
|
|
||||||
|
@Column(name = "comments", columnDefinition = "TEXT")
|
||||||
|
private String comments;
|
||||||
|
|
||||||
|
@Column(name = "designation", columnDefinition = "TEXT")
|
||||||
|
private String designation;
|
||||||
|
|
||||||
|
@Column(name = "date_of_birth", length = 500)
|
||||||
|
private String dateOfBirth;
|
||||||
|
|
||||||
|
@Column(name = "place_of_birth", length = 500)
|
||||||
|
private String placeOfBirth;
|
||||||
|
|
||||||
|
@Column(name = "is_active", nullable = false)
|
||||||
|
private boolean active = true;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "sanctionsEntry", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||||
|
private List<SanctionsAlias> aliases = new ArrayList<>();
|
||||||
|
|
||||||
|
protected SanctionsEntry() {}
|
||||||
|
|
||||||
|
public SanctionsEntry(final String dataId, final String entryType, final String firstName,
|
||||||
|
final String secondName, final String thirdName, final String fourthName,
|
||||||
|
final String unListType, final String referenceNumber, final LocalDate listedOn,
|
||||||
|
final String gender, final String nationality, final String comments,
|
||||||
|
final String designation, final String dateOfBirth, final String placeOfBirth) {
|
||||||
|
this.dataId = dataId;
|
||||||
|
this.entryType = entryType;
|
||||||
|
this.firstName = firstName;
|
||||||
|
this.secondName = secondName;
|
||||||
|
this.thirdName = thirdName;
|
||||||
|
this.fourthName = fourthName;
|
||||||
|
this.fullName = buildFullName(firstName, secondName, thirdName, fourthName);
|
||||||
|
this.unListType = unListType;
|
||||||
|
this.referenceNumber = referenceNumber;
|
||||||
|
this.listedOn = listedOn;
|
||||||
|
this.gender = gender;
|
||||||
|
this.nationality = nationality;
|
||||||
|
this.comments = comments;
|
||||||
|
this.designation = designation;
|
||||||
|
this.dateOfBirth = dateOfBirth;
|
||||||
|
this.placeOfBirth = placeOfBirth;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildFullName(String... parts) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (String part : parts) {
|
||||||
|
if (part != null && !part.isBlank()) {
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
sb.append(" ");
|
||||||
|
}
|
||||||
|
sb.append(part.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.length() > 0 ? sb.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAlias(SanctionsAlias alias) {
|
||||||
|
aliases.add(alias);
|
||||||
|
alias.setSanctionsEntry(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Accessors ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public String getDataId() { return dataId; }
|
||||||
|
public String getEntryType() { return entryType; }
|
||||||
|
public String getFirstName() { return firstName; }
|
||||||
|
public String getSecondName() { return secondName; }
|
||||||
|
public String getThirdName() { return thirdName; }
|
||||||
|
public String getFourthName() { return fourthName; }
|
||||||
|
public String getFullName() { return fullName; }
|
||||||
|
public String getUnListType() { return unListType; }
|
||||||
|
public String getReferenceNumber() { return referenceNumber; }
|
||||||
|
public LocalDate getListedOn() { return listedOn; }
|
||||||
|
public String getGender() { return gender; }
|
||||||
|
public String getNationality() { return nationality; }
|
||||||
|
public String getComments() { return comments; }
|
||||||
|
public String getDesignation() { return designation; }
|
||||||
|
public String getDateOfBirth() { return dateOfBirth; }
|
||||||
|
public String getPlaceOfBirth() { return placeOfBirth; }
|
||||||
|
public boolean isActive() { return active; }
|
||||||
|
public List<SanctionsAlias> getAliases() { return aliases; }
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.sanctions.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;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface SanctionsEntryRepository extends JpaRepository<SanctionsEntry, Long> {
|
||||||
|
|
||||||
|
@Query("SELECT s FROM SanctionsEntry s WHERE s.active = true AND LOWER(s.fullName) LIKE LOWER(CONCAT('%', :name, '%'))")
|
||||||
|
List<SanctionsEntry> searchByFullName(@Param("name") String name);
|
||||||
|
|
||||||
|
@Query("SELECT DISTINCT s FROM SanctionsEntry s LEFT JOIN s.aliases a WHERE s.active = true " +
|
||||||
|
"AND (LOWER(s.fullName) LIKE LOWER(CONCAT('%', :name, '%')) " +
|
||||||
|
"OR LOWER(a.aliasName) LIKE LOWER(CONCAT('%', :name, '%')))")
|
||||||
|
List<SanctionsEntry> searchByNameOrAlias(@Param("name") String name);
|
||||||
|
|
||||||
|
boolean existsByDataId(String dataId);
|
||||||
|
|
||||||
|
void deleteAllByActiveTrue();
|
||||||
|
|
||||||
|
long countByActiveTrue();
|
||||||
|
}
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.sanctions.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity log for each sanctions screening request.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_sanctions_screening_log")
|
||||||
|
public class SanctionsScreeningLog extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@Column(name = "screened_name", nullable = false, length = 500)
|
||||||
|
private String screenedName;
|
||||||
|
|
||||||
|
/** FLAGGED or CLEAR */
|
||||||
|
@Column(name = "screening_result", nullable = false, length = 20)
|
||||||
|
private String screeningResult;
|
||||||
|
|
||||||
|
@Column(name = "match_count", nullable = false)
|
||||||
|
private Integer matchCount;
|
||||||
|
|
||||||
|
@Column(name = "matched_entry_ids", length = 1000)
|
||||||
|
private String matchedEntryIds;
|
||||||
|
|
||||||
|
@Column(name = "matched_names", length = 2000)
|
||||||
|
private String matchedNames;
|
||||||
|
|
||||||
|
/** CLIENT_CREATION, MANUAL_CHECK, API */
|
||||||
|
@Column(name = "screening_source", length = 50)
|
||||||
|
private String screeningSource;
|
||||||
|
|
||||||
|
@Column(name = "client_id")
|
||||||
|
private Long clientId;
|
||||||
|
|
||||||
|
@Column(name = "ip_address", length = 50)
|
||||||
|
private String ipAddress;
|
||||||
|
|
||||||
|
@Column(name = "screened_on", nullable = false)
|
||||||
|
private OffsetDateTime screenedOn;
|
||||||
|
|
||||||
|
protected SanctionsScreeningLog() {}
|
||||||
|
|
||||||
|
public SanctionsScreeningLog(final String screenedName, final String screeningResult,
|
||||||
|
final Integer matchCount, final String matchedEntryIds, final String matchedNames,
|
||||||
|
final String screeningSource, final Long clientId, final String ipAddress) {
|
||||||
|
this.screenedName = screenedName;
|
||||||
|
this.screeningResult = screeningResult;
|
||||||
|
this.matchCount = matchCount;
|
||||||
|
this.matchedEntryIds = matchedEntryIds;
|
||||||
|
this.matchedNames = matchedNames;
|
||||||
|
this.screeningSource = screeningSource;
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.ipAddress = ipAddress;
|
||||||
|
this.screenedOn = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Accessors ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public String getScreenedName() { return screenedName; }
|
||||||
|
public String getScreeningResult() { return screeningResult; }
|
||||||
|
public Integer getMatchCount() { return matchCount; }
|
||||||
|
public String getMatchedEntryIds() { return matchedEntryIds; }
|
||||||
|
public String getMatchedNames() { return matchedNames; }
|
||||||
|
public String getScreeningSource() { return screeningSource; }
|
||||||
|
public Long getClientId() { return clientId; }
|
||||||
|
public String getIpAddress() { return ipAddress; }
|
||||||
|
public OffsetDateTime getScreenedOn() { return screenedOn; }
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.sanctions.domain;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
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;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface SanctionsScreeningLogRepository extends JpaRepository<SanctionsScreeningLog, Long> {
|
||||||
|
|
||||||
|
@Query("SELECT l FROM SanctionsScreeningLog l ORDER BY l.screenedOn DESC")
|
||||||
|
List<SanctionsScreeningLog> findAllOrderByScreenedOnDesc();
|
||||||
|
|
||||||
|
@Query("SELECT l FROM SanctionsScreeningLog l WHERE l.screeningResult = :result ORDER BY l.screenedOn DESC")
|
||||||
|
List<SanctionsScreeningLog> findByScreeningResult(@Param("result") String result);
|
||||||
|
|
||||||
|
@Query("SELECT l FROM SanctionsScreeningLog l WHERE l.screenedOn >= :from AND l.screenedOn <= :to ORDER BY l.screenedOn DESC")
|
||||||
|
List<SanctionsScreeningLog> findByDateRange(@Param("from") OffsetDateTime from, @Param("to") OffsetDateTime to);
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
+187
@@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.openproject.api;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.DefaultValue;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.Context;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.UriInfo;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||||
|
import org.apache.fineract.infrastructure.openproject.data.OpenProjectProjectData;
|
||||||
|
import org.apache.fineract.infrastructure.openproject.data.OpenProjectStatusSummaryData;
|
||||||
|
import org.apache.fineract.infrastructure.openproject.data.OpenProjectWorkPackageData;
|
||||||
|
import org.apache.fineract.infrastructure.openproject.service.OpenProjectService;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/openproject")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "OpenProject", description = "Integration with OpenProject for project management. "
|
||||||
|
+ "Provides access to project details, work packages, and status summaries.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class OpenProjectApiResource {
|
||||||
|
|
||||||
|
private static final Set<String> PROJECT_RESPONSE_PARAMS = new HashSet<>(
|
||||||
|
Arrays.asList("id", "identifier", "name", "active", "description", "createdAt", "updatedAt"));
|
||||||
|
private static final Set<String> WORK_PACKAGE_RESPONSE_PARAMS = new HashSet<>(
|
||||||
|
Arrays.asList("id", "subject", "description", "typeName", "statusName", "priorityName",
|
||||||
|
"assigneeName", "percentageDone", "startDate", "dueDate", "createdAt", "updatedAt"));
|
||||||
|
private static final Set<String> SUMMARY_RESPONSE_PARAMS = new HashSet<>(
|
||||||
|
Arrays.asList("totalWorkPackages", "byStatus", "byType", "byPriority"));
|
||||||
|
|
||||||
|
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "OPENPROJECT";
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final OpenProjectService openProjectService;
|
||||||
|
private final DefaultToApiJsonSerializer<OpenProjectProjectData> projectSerializer;
|
||||||
|
private final DefaultToApiJsonSerializer<OpenProjectWorkPackageData> workPackageSerializer;
|
||||||
|
private final DefaultToApiJsonSerializer<OpenProjectStatusSummaryData> summarySerializer;
|
||||||
|
private final ApiRequestParameterHelper apiRequestParameterHelper;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("project")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve OpenProject project details",
|
||||||
|
description = "Returns the configured OpenProject project information.")
|
||||||
|
@ApiResponse(responseCode = "200", description = "OK",
|
||||||
|
content = @Content(schema = @Schema(implementation = OpenProjectProjectData.class)))
|
||||||
|
public String getProject(@Context final UriInfo uriInfo) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
log.info("OpenProject API - Fetching project details");
|
||||||
|
|
||||||
|
OpenProjectProjectData project = openProjectService.getProject();
|
||||||
|
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||||
|
return this.projectSerializer.serialize(settings, project, PROJECT_RESPONSE_PARAMS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("work-packages")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve work packages",
|
||||||
|
description = "Returns work packages from the configured OpenProject project.\n\n"
|
||||||
|
+ "Optional filters: status (open/closed), type name.\n\n"
|
||||||
|
+ "Example: openproject/work-packages?status=open&pageSize=25&offset=1")
|
||||||
|
@ApiResponse(responseCode = "200", description = "OK",
|
||||||
|
content = @Content(array = @ArraySchema(schema = @Schema(implementation = OpenProjectWorkPackageData.class))))
|
||||||
|
public String getWorkPackages(
|
||||||
|
@QueryParam("status") @Parameter(description = "Filter by status: open or closed") final String status,
|
||||||
|
@QueryParam("type") @Parameter(description = "Filter by type name") final String type,
|
||||||
|
@QueryParam("pageSize") @DefaultValue("25") @Parameter(description = "Page size") final int pageSize,
|
||||||
|
@QueryParam("offset") @DefaultValue("1") @Parameter(description = "Page offset") final int offset,
|
||||||
|
@Context final UriInfo uriInfo) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
log.info("OpenProject API - Fetching work packages (status={}, type={}, pageSize={}, offset={})", status, type, pageSize, offset);
|
||||||
|
|
||||||
|
Collection<OpenProjectWorkPackageData> workPackages = openProjectService.getWorkPackages(status, type, pageSize, offset);
|
||||||
|
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||||
|
return this.workPackageSerializer.serialize(settings, workPackages, WORK_PACKAGE_RESPONSE_PARAMS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("work-packages/{workPackageId}")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve a single work package by ID",
|
||||||
|
description = "Returns work package details from OpenProject by its ID.")
|
||||||
|
@ApiResponse(responseCode = "200", description = "OK",
|
||||||
|
content = @Content(schema = @Schema(implementation = OpenProjectWorkPackageData.class)))
|
||||||
|
public String getWorkPackageById(
|
||||||
|
@PathParam("workPackageId") @Parameter(description = "Work package ID") final Long workPackageId,
|
||||||
|
@Context final UriInfo uriInfo) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
log.info("OpenProject API - Fetching work package by ID: {}", workPackageId);
|
||||||
|
|
||||||
|
OpenProjectWorkPackageData workPackage = openProjectService.getWorkPackageById(workPackageId);
|
||||||
|
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||||
|
return this.workPackageSerializer.serialize(settings, workPackage, WORK_PACKAGE_RESPONSE_PARAMS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("summary")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve project status summary",
|
||||||
|
description = "Returns a summary of all work packages grouped by status, type, and priority.")
|
||||||
|
@ApiResponse(responseCode = "200", description = "OK",
|
||||||
|
content = @Content(schema = @Schema(implementation = OpenProjectStatusSummaryData.class)))
|
||||||
|
public String getStatusSummary(@Context final UriInfo uriInfo) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
log.info("OpenProject API - Fetching status summary");
|
||||||
|
|
||||||
|
OpenProjectStatusSummaryData summary = openProjectService.getStatusSummary();
|
||||||
|
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||||
|
return this.summarySerializer.serialize(settings, summary, SUMMARY_RESPONSE_PARAMS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("work-packages")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Create a new work package",
|
||||||
|
description = "Creates a new work package in OpenProject.\n\n"
|
||||||
|
+ "Request body: {\"subject\": \"Task title\", \"description\": \"Details\", \"type\": \"Task\"}\n\n"
|
||||||
|
+ "Valid types: Task, Milestone, Feature, Epic, User story, Bug, Incident, Support Ticket, etc.")
|
||||||
|
@ApiResponse(responseCode = "200", description = "OK",
|
||||||
|
content = @Content(schema = @Schema(implementation = OpenProjectWorkPackageData.class)))
|
||||||
|
public String createWorkPackage(@Parameter(hidden = true) final String jsonRequestBody,
|
||||||
|
@Context final UriInfo uriInfo) {
|
||||||
|
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
log.info("OpenProject API - Creating work package");
|
||||||
|
|
||||||
|
JsonObject request = JsonParser.parseString(jsonRequestBody).getAsJsonObject();
|
||||||
|
String subject = request.has("subject") ? request.get("subject").getAsString() : null;
|
||||||
|
String description = request.has("description") ? request.get("description").getAsString() : null;
|
||||||
|
String type = request.has("type") ? request.get("type").getAsString() : "Task";
|
||||||
|
|
||||||
|
if (subject == null || subject.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("subject is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenProjectWorkPackageData created = openProjectService.createWorkPackage(subject, description, type);
|
||||||
|
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||||
|
return this.workPackageSerializer.serialize(settings, created, WORK_PACKAGE_RESPONSE_PARAMS);
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -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.infrastructure.openproject.service;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "fineract.openproject")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class OpenProjectProperties {
|
||||||
|
|
||||||
|
private boolean enabled = true;
|
||||||
|
private String baseUrl;
|
||||||
|
private String projectId;
|
||||||
|
private String apiToken;
|
||||||
|
}
|
||||||
+288
@@ -0,0 +1,288 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.openproject.service;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import okhttp3.Credentials;
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.ResourceNotFoundException;
|
||||||
|
import org.apache.fineract.infrastructure.openproject.data.OpenProjectProjectData;
|
||||||
|
import org.apache.fineract.infrastructure.openproject.data.OpenProjectStatusSummaryData;
|
||||||
|
import org.apache.fineract.infrastructure.openproject.data.OpenProjectWorkPackageData;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class OpenProjectService {
|
||||||
|
|
||||||
|
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
|
||||||
|
|
||||||
|
private final OkHttpClient httpClient;
|
||||||
|
private final OpenProjectProperties properties;
|
||||||
|
private final String credential;
|
||||||
|
|
||||||
|
public OpenProjectService(OkHttpClient httpClient, OpenProjectProperties properties) {
|
||||||
|
this.httpClient = httpClient;
|
||||||
|
this.properties = properties;
|
||||||
|
this.credential = Credentials.basic("apikey", properties.getApiToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpenProjectProjectData getProject() {
|
||||||
|
String url = properties.getBaseUrl() + "/api/v3/projects/" + properties.getProjectId();
|
||||||
|
JsonObject json = executeGet(url);
|
||||||
|
|
||||||
|
OpenProjectProjectData data = new OpenProjectProjectData();
|
||||||
|
data.setId(json.get("id").getAsLong());
|
||||||
|
data.setIdentifier(getStringOrNull(json, "identifier"));
|
||||||
|
data.setName(getStringOrNull(json, "name"));
|
||||||
|
data.setActive(json.has("active") && json.get("active").getAsBoolean());
|
||||||
|
data.setCreatedAt(getStringOrNull(json, "createdAt"));
|
||||||
|
data.setUpdatedAt(getStringOrNull(json, "updatedAt"));
|
||||||
|
|
||||||
|
if (json.has("description") && json.get("description").isJsonObject()) {
|
||||||
|
data.setDescription(getStringOrNull(json.getAsJsonObject("description"), "raw"));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<OpenProjectWorkPackageData> getWorkPackages(String status, String type, int pageSize, int offset) {
|
||||||
|
StringBuilder url = new StringBuilder(properties.getBaseUrl())
|
||||||
|
.append("/api/v3/projects/").append(properties.getProjectId())
|
||||||
|
.append("/work_packages?pageSize=").append(pageSize)
|
||||||
|
.append("&offset=").append(offset);
|
||||||
|
|
||||||
|
// Build filters
|
||||||
|
JsonArray filters = new JsonArray();
|
||||||
|
if (status != null && !status.isBlank()) {
|
||||||
|
// Filter by status name — requires status ID lookup, so we use open/closed shorthand
|
||||||
|
if ("open".equalsIgnoreCase(status)) {
|
||||||
|
JsonObject f = new JsonObject();
|
||||||
|
JsonObject op = new JsonObject();
|
||||||
|
op.addProperty("operator", "o");
|
||||||
|
op.add("values", new JsonArray());
|
||||||
|
f.add("status", op);
|
||||||
|
filters.add(f);
|
||||||
|
} else if ("closed".equalsIgnoreCase(status)) {
|
||||||
|
JsonObject f = new JsonObject();
|
||||||
|
JsonObject op = new JsonObject();
|
||||||
|
op.addProperty("operator", "c");
|
||||||
|
op.add("values", new JsonArray());
|
||||||
|
f.add("status", op);
|
||||||
|
filters.add(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.size() > 0) {
|
||||||
|
url.append("&filters=").append(filters.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject json = executeGet(url.toString());
|
||||||
|
return parseWorkPackages(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<OpenProjectWorkPackageData> getAllWorkPackages() {
|
||||||
|
// Fetch all (no status filter) with large page size
|
||||||
|
String url = properties.getBaseUrl()
|
||||||
|
+ "/api/v3/projects/" + properties.getProjectId()
|
||||||
|
+ "/work_packages?pageSize=200&filters="
|
||||||
|
+ "[{\"status\":{\"operator\":\"*\",\"values\":[]}}]";
|
||||||
|
JsonObject json = executeGet(url);
|
||||||
|
return parseWorkPackages(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpenProjectStatusSummaryData getStatusSummary() {
|
||||||
|
Collection<OpenProjectWorkPackageData> all = getAllWorkPackages();
|
||||||
|
|
||||||
|
Map<String, Integer> byStatus = new LinkedHashMap<>();
|
||||||
|
Map<String, Integer> byType = new LinkedHashMap<>();
|
||||||
|
Map<String, Integer> byPriority = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
for (OpenProjectWorkPackageData wp : all) {
|
||||||
|
byStatus.merge(wp.getStatusName() != null ? wp.getStatusName() : "Unknown", 1, Integer::sum);
|
||||||
|
byType.merge(wp.getTypeName() != null ? wp.getTypeName() : "Unknown", 1, Integer::sum);
|
||||||
|
byPriority.merge(wp.getPriorityName() != null ? wp.getPriorityName() : "Unknown", 1, Integer::sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenProjectStatusSummaryData summary = new OpenProjectStatusSummaryData();
|
||||||
|
summary.setTotalWorkPackages(all.size());
|
||||||
|
summary.setByStatus(byStatus);
|
||||||
|
summary.setByType(byType);
|
||||||
|
summary.setByPriority(byPriority);
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpenProjectWorkPackageData getWorkPackageById(Long workPackageId) {
|
||||||
|
String url = properties.getBaseUrl() + "/api/v3/work_packages/" + workPackageId;
|
||||||
|
JsonObject json = executeGet(url);
|
||||||
|
return parseSingleWorkPackage(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpenProjectWorkPackageData createWorkPackage(String subject, String description, String typeName) {
|
||||||
|
String url = properties.getBaseUrl()
|
||||||
|
+ "/api/v3/projects/" + properties.getProjectId() + "/work_packages";
|
||||||
|
|
||||||
|
JsonObject body = new JsonObject();
|
||||||
|
body.addProperty("subject", subject);
|
||||||
|
|
||||||
|
if (description != null && !description.isBlank()) {
|
||||||
|
JsonObject desc = new JsonObject();
|
||||||
|
desc.addProperty("format", "markdown");
|
||||||
|
desc.addProperty("raw", description);
|
||||||
|
body.add("description", desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set required custom field: "Last Follow-up Date" (customField8)
|
||||||
|
body.addProperty("customField8", LocalDate.now().toString());
|
||||||
|
|
||||||
|
if (typeName != null && !typeName.isBlank()) {
|
||||||
|
JsonObject links = new JsonObject();
|
||||||
|
Long typeId = resolveTypeId(typeName);
|
||||||
|
if (typeId != null) {
|
||||||
|
JsonObject typeLink = new JsonObject();
|
||||||
|
typeLink.addProperty("href", "/api/v3/types/" + typeId);
|
||||||
|
links.add("type", typeLink);
|
||||||
|
}
|
||||||
|
body.add("_links", links);
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject json = executePost(url, body.toString());
|
||||||
|
return parseSingleWorkPackage(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long resolveTypeId(String typeName) {
|
||||||
|
String url = properties.getBaseUrl()
|
||||||
|
+ "/api/v3/projects/" + properties.getProjectId() + "/types";
|
||||||
|
JsonObject json = executeGet(url);
|
||||||
|
JsonArray elements = json.getAsJsonObject("_embedded").getAsJsonArray("elements");
|
||||||
|
for (JsonElement el : elements) {
|
||||||
|
JsonObject t = el.getAsJsonObject();
|
||||||
|
if (typeName.equalsIgnoreCase(getStringOrNull(t, "name"))) {
|
||||||
|
return t.get("id").getAsLong();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<OpenProjectWorkPackageData> parseWorkPackages(JsonObject json) {
|
||||||
|
Collection<OpenProjectWorkPackageData> result = new ArrayList<>();
|
||||||
|
if (!json.has("_embedded")) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
JsonArray elements = json.getAsJsonObject("_embedded").getAsJsonArray("elements");
|
||||||
|
for (JsonElement el : elements) {
|
||||||
|
result.add(parseSingleWorkPackage(el.getAsJsonObject()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OpenProjectWorkPackageData parseSingleWorkPackage(JsonObject wp) {
|
||||||
|
OpenProjectWorkPackageData data = new OpenProjectWorkPackageData();
|
||||||
|
data.setId(wp.get("id").getAsLong());
|
||||||
|
data.setSubject(getStringOrNull(wp, "subject"));
|
||||||
|
data.setPercentageDone(wp.has("percentageDone") && !wp.get("percentageDone").isJsonNull()
|
||||||
|
? wp.get("percentageDone").getAsInt() : 0);
|
||||||
|
data.setStartDate(getStringOrNull(wp, "startDate"));
|
||||||
|
data.setDueDate(getStringOrNull(wp, "dueDate"));
|
||||||
|
data.setCreatedAt(getStringOrNull(wp, "createdAt"));
|
||||||
|
data.setUpdatedAt(getStringOrNull(wp, "updatedAt"));
|
||||||
|
|
||||||
|
if (wp.has("description") && wp.get("description").isJsonObject()) {
|
||||||
|
data.setDescription(getStringOrNull(wp.getAsJsonObject("description"), "raw"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract linked names from _links
|
||||||
|
if (wp.has("_links") && wp.get("_links").isJsonObject()) {
|
||||||
|
JsonObject links = wp.getAsJsonObject("_links");
|
||||||
|
data.setTypeName(getLinkTitle(links, "type"));
|
||||||
|
data.setStatusName(getLinkTitle(links, "status"));
|
||||||
|
data.setPriorityName(getLinkTitle(links, "priority"));
|
||||||
|
data.setAssigneeName(getLinkTitle(links, "assignee"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getLinkTitle(JsonObject links, String key) {
|
||||||
|
if (links.has(key) && links.get(key).isJsonObject()) {
|
||||||
|
return getStringOrNull(links.getAsJsonObject(key), "title");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject executeGet(String url) {
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.header("Authorization", credential)
|
||||||
|
.get()
|
||||||
|
.build();
|
||||||
|
return execute(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject executePost(String url, String jsonBody) {
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.header("Authorization", credential)
|
||||||
|
.post(RequestBody.create(jsonBody, JSON))
|
||||||
|
.build();
|
||||||
|
return execute(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject execute(Request request) {
|
||||||
|
try (Response response = httpClient.newCall(request).execute()) {
|
||||||
|
String body = response.body() != null ? response.body().string() : "";
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
log.error("OpenProject API error: {} - {}", response.code(), body);
|
||||||
|
if (response.code() == 404) {
|
||||||
|
throw new ResourceNotFoundException("error.msg.openproject.not.found",
|
||||||
|
"OpenProject resource not found: " + body, new Object[] {});
|
||||||
|
}
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.openproject.api.error",
|
||||||
|
"OpenProject API returned HTTP " + response.code() + ": " + body);
|
||||||
|
}
|
||||||
|
return JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("OpenProject API connection error", e);
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.openproject.connection.failed",
|
||||||
|
"Failed to connect to OpenProject: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getStringOrNull(JsonObject json, String key) {
|
||||||
|
if (json.has(key) && !json.get(key).isJsonNull()) {
|
||||||
|
return json.get(key).getAsString();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.rbifeed.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.Context;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.UriInfo;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||||
|
import org.apache.fineract.infrastructure.rbifeed.data.RbiFeedItemData;
|
||||||
|
import org.apache.fineract.infrastructure.rbifeed.service.RbiFeedFetchService;
|
||||||
|
import org.apache.fineract.infrastructure.rbifeed.service.RbiFeedReadPlatformService;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/rbi-feeds")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "RBI Feeds", description = "RBI RSS feed items fetched from rbi.org.in. Categories: PRESS_RELEASE, NOTIFICATION, SPEECH, TENDER, PUBLICATION")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class RbiFeedApiResource {
|
||||||
|
|
||||||
|
private static final Set<String> RESPONSE_DATA_PARAMETERS = new HashSet<>(
|
||||||
|
Arrays.asList("id", "title", "description", "link", "pubDate", "category", "fetchedOn",
|
||||||
|
"openProjectWpId", "openProjectWpStatus", "openProjectWpProgress"));
|
||||||
|
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "RBI_FEED";
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final RbiFeedReadPlatformService readPlatformService;
|
||||||
|
private final RbiFeedFetchService fetchService;
|
||||||
|
private final DefaultToApiJsonSerializer<RbiFeedItemData> toApiJsonSerializer;
|
||||||
|
private final ApiRequestParameterHelper apiRequestParameterHelper;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve all RBI Feed Items", description = "Returns all RBI RSS feed items, optionally filtered by category.\n\n"
|
||||||
|
+ "Valid categories: PRESS_RELEASE, NOTIFICATION, SPEECH, TENDER, PUBLICATION\n\n"
|
||||||
|
+ "Example Requests:\n\nrbi-feeds\n\nrbi-feeds?category=PRESS_RELEASE")
|
||||||
|
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = RbiFeedItemData.class))))
|
||||||
|
public String retrieveAll(@QueryParam("category") @Parameter(description = "Filter by category") final String category,
|
||||||
|
@Context final UriInfo uriInfo) {
|
||||||
|
log.info("RBI Feed API - retrieveAll called with category: {}", category);
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
|
||||||
|
final Collection<RbiFeedItemData> feedItems;
|
||||||
|
if (category != null && !category.isBlank()) {
|
||||||
|
log.debug("RBI Feed API - Fetching feeds for category: {}", category);
|
||||||
|
feedItems = this.readPlatformService.retrieveByCategory(category);
|
||||||
|
} else {
|
||||||
|
log.debug("RBI Feed API - Fetching all feeds");
|
||||||
|
feedItems = this.readPlatformService.retrieveAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("RBI Feed API - Returning {} feed items", feedItems.size());
|
||||||
|
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||||
|
return this.toApiJsonSerializer.serialize(settings, feedItems, RESPONSE_DATA_PARAMETERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{feedItemId}")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve a single RBI Feed Item", description = "Returns a specific RBI RSS feed item by ID.\n\n"
|
||||||
|
+ "Example Requests:\n\nrbi-feeds/1")
|
||||||
|
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RbiFeedItemData.class)))
|
||||||
|
public String retrieveOne(@PathParam("feedItemId") @Parameter(description = "feedItemId") final Long feedItemId,
|
||||||
|
@Context final UriInfo uriInfo) {
|
||||||
|
log.info("RBI Feed API - retrieveOne called with feedItemId: {}", feedItemId);
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
|
||||||
|
final RbiFeedItemData feedItem = this.readPlatformService.retrieveOne(feedItemId);
|
||||||
|
log.debug("RBI Feed API - Found feed item: {}", feedItem != null ? feedItem.getTitle() : "null");
|
||||||
|
|
||||||
|
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||||
|
return this.toApiJsonSerializer.serialize(settings, feedItem, RESPONSE_DATA_PARAMETERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("fetch")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Manually trigger RBI feed fetch", description = "Fetches latest RSS feeds from RBI website and stores new items in the database.\n\n"
|
||||||
|
+ "For NOTIFICATION category items, OpenProject work packages are automatically created and linked.")
|
||||||
|
@ApiResponse(responseCode = "200", description = "OK")
|
||||||
|
public String fetchFeeds(@Parameter(hidden = true) final String jsonRequestBody) {
|
||||||
|
log.info("RBI Feed API - Manual fetch triggered");
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
log.debug("RBI Feed API - User authenticated, starting feed fetch...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
int newItems = this.fetchService.fetchAndStoreAllFeeds();
|
||||||
|
log.info("RBI Feed API - Fetch completed. {} new items stored.", newItems);
|
||||||
|
return this.toApiJsonSerializer.serialize(java.util.Map.of("newItemsFetched", newItems));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("RBI Feed API - Error during feed fetch", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("sync-openproject")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Sync OpenProject work package status",
|
||||||
|
description = "Updates the OpenProject work package status and progress for all linked NOTIFICATION feed items.")
|
||||||
|
@ApiResponse(responseCode = "200", description = "OK")
|
||||||
|
public String syncOpenProjectStatus(@Parameter(hidden = true) final String jsonRequestBody) {
|
||||||
|
log.info("RBI Feed API - OpenProject status sync triggered");
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
|
||||||
|
int updated = this.fetchService.syncOpenProjectStatus();
|
||||||
|
return this.toApiJsonSerializer.serialize(java.util.Map.of("itemsUpdated", updated));
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.rbifeed.jobs.fetchrbifeeds;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.jobs.service.JobName;
|
||||||
|
import org.springframework.batch.core.Job;
|
||||||
|
import org.springframework.batch.core.Step;
|
||||||
|
import org.springframework.batch.core.job.builder.JobBuilder;
|
||||||
|
import org.springframework.batch.core.launch.support.RunIdIncrementer;
|
||||||
|
import org.springframework.batch.core.repository.JobRepository;
|
||||||
|
import org.springframework.batch.core.step.builder.StepBuilder;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class FetchRbiFeedsConfig {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JobRepository jobRepository;
|
||||||
|
@Autowired
|
||||||
|
private PlatformTransactionManager transactionManager;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
protected Step fetchRbiFeedsStep(FetchRbiFeedsTasklet fetchRbiFeedsTasklet) {
|
||||||
|
return new StepBuilder(JobName.FETCH_RBI_RSS_FEEDS.name(), jobRepository)
|
||||||
|
.tasklet(fetchRbiFeedsTasklet, transactionManager).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Job fetchRbiFeedsJob(FetchRbiFeedsTasklet fetchRbiFeedsTasklet) {
|
||||||
|
return new JobBuilder(JobName.FETCH_RBI_RSS_FEEDS.name(), jobRepository)
|
||||||
|
.start(fetchRbiFeedsStep(fetchRbiFeedsTasklet)).incrementer(new RunIdIncrementer()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.infrastructure.rbifeed.jobs.fetchrbifeeds;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.infrastructure.rbifeed.service.RbiFeedFetchService;
|
||||||
|
import org.springframework.batch.core.StepContribution;
|
||||||
|
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||||
|
import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||||
|
import org.springframework.batch.repeat.RepeatStatus;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class FetchRbiFeedsTasklet implements Tasklet {
|
||||||
|
|
||||||
|
private final RbiFeedFetchService rbiFeedFetchService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
|
||||||
|
log.info("Starting RBI RSS feed fetch job...");
|
||||||
|
int newItems = rbiFeedFetchService.fetchAndStoreAllFeeds();
|
||||||
|
log.info("RBI RSS feed fetch job completed. {} new items stored.", newItems);
|
||||||
|
return RepeatStatus.FINISHED;
|
||||||
|
}
|
||||||
|
}
|
||||||
+269
@@ -0,0 +1,269 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.rbifeed.service;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeFormatterBuilder;
|
||||||
|
import java.time.temporal.ChronoField;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.infrastructure.openproject.data.OpenProjectWorkPackageData;
|
||||||
|
import org.apache.fineract.infrastructure.openproject.service.OpenProjectProperties;
|
||||||
|
import org.apache.fineract.infrastructure.openproject.service.OpenProjectService;
|
||||||
|
import org.apache.fineract.infrastructure.rbifeed.domain.RbiFeedItem;
|
||||||
|
import org.apache.fineract.infrastructure.rbifeed.domain.RbiFeedItemRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class RbiFeedFetchService {
|
||||||
|
|
||||||
|
private final RbiFeedItemRepository repository;
|
||||||
|
private final OpenProjectService openProjectService;
|
||||||
|
private final OpenProjectProperties openProjectProperties;
|
||||||
|
private final EntityManager entityManager;
|
||||||
|
|
||||||
|
private static final String NOTIFICATION_CATEGORY = "NOTIFICATION";
|
||||||
|
|
||||||
|
private static final Map<String, String> FEED_URLS = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
FEED_URLS.put("PRESS_RELEASE", "https://rbi.org.in/pressreleases_rss.xml");
|
||||||
|
FEED_URLS.put("NOTIFICATION", "https://rbi.org.in/notifications_rss.xml");
|
||||||
|
FEED_URLS.put("SPEECH", "https://rbi.org.in/speeches_rss.xml");
|
||||||
|
FEED_URLS.put("TENDER", "https://rbi.org.in/tenders_rss.xml");
|
||||||
|
FEED_URLS.put("PUBLICATION", "https://rbi.org.in/Publication_rss.xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final DateTimeFormatter RBI_DATE_FORMAT = new DateTimeFormatterBuilder()
|
||||||
|
.appendPattern("EEE, dd MMM yyyy HH:mm:ss")
|
||||||
|
.optionalStart().appendPattern(" Z").optionalEnd()
|
||||||
|
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
|
||||||
|
.toFormatter(Locale.ENGLISH);
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public int fetchAndStoreAllFeeds() {
|
||||||
|
// Clear Hibernate persistence context so existsByLink() queries hit the DB directly.
|
||||||
|
// This fixes the issue where externally truncating m_rbi_feed_item wouldn't be seen
|
||||||
|
// due to stale cached entities.
|
||||||
|
entityManager.clear();
|
||||||
|
log.info("RBI FeedFetchService - Starting fetch for all {} feed categories", FEED_URLS.size());
|
||||||
|
int totalNewItems = 0;
|
||||||
|
for (Map.Entry<String, String> entry : FEED_URLS.entrySet()) {
|
||||||
|
try {
|
||||||
|
int count = fetchAndStoreFeed(entry.getKey(), entry.getValue());
|
||||||
|
totalNewItems += count;
|
||||||
|
log.info("RBI FeedFetchService - Category {} completed: {} new items", entry.getKey(), count);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("RBI FeedFetchService - Failed to fetch for category: {} from URL: {}", entry.getKey(), entry.getValue(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("RBI FeedFetchService - All feeds fetched. Total new items: {}", totalNewItems);
|
||||||
|
|
||||||
|
// Create OpenProject work packages for any NOTIFICATION items that don't have one yet
|
||||||
|
// (e.g. items fetched before OpenProject integration was enabled)
|
||||||
|
if (openProjectProperties.isEnabled()) {
|
||||||
|
List<RbiFeedItem> unlinkedNotifications = repository.findByCategoryAndOpenProjectWpIdIsNull(NOTIFICATION_CATEGORY);
|
||||||
|
if (!unlinkedNotifications.isEmpty()) {
|
||||||
|
log.info("RBI FeedFetchService - Found {} existing NOTIFICATION items without OpenProject WP, creating now",
|
||||||
|
unlinkedNotifications.size());
|
||||||
|
createOpenProjectWorkPackages(unlinkedNotifications);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalNewItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int fetchAndStoreFeed(final String category, final String url) throws Exception {
|
||||||
|
log.debug("RBI FeedFetchService - Fetching RSS from URL: {}", url);
|
||||||
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
|
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url))
|
||||||
|
.header("User-Agent", "Fineract-CBS/1.0").GET().build();
|
||||||
|
HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
||||||
|
log.debug("RBI FeedFetchService - HTTP response status: {} for {}", response.statusCode(), url);
|
||||||
|
|
||||||
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
|
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||||
|
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
||||||
|
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
||||||
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||||
|
Document doc = builder.parse(response.body());
|
||||||
|
|
||||||
|
NodeList items = doc.getElementsByTagName("item");
|
||||||
|
log.debug("RBI FeedFetchService - Parsed {} items from RSS for category: {}", items.getLength(), category);
|
||||||
|
List<RbiFeedItem> newItems = new ArrayList<>();
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
for (int i = 0; i < items.getLength(); i++) {
|
||||||
|
Element item = (Element) items.item(i);
|
||||||
|
String title = getElementText(item, "title");
|
||||||
|
String description = getElementText(item, "description");
|
||||||
|
String link = getElementText(item, "link");
|
||||||
|
String pubDateStr = getElementText(item, "pubDate");
|
||||||
|
|
||||||
|
if (link == null) {
|
||||||
|
log.warn("RBI FeedFetchService - Skipping item with null link, title: {}", title);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (repository.existsByLink(link)) {
|
||||||
|
log.trace("RBI FeedFetchService - Skipping existing item: {}", link);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime pubDate = null;
|
||||||
|
if (pubDateStr != null && !pubDateStr.isBlank()) {
|
||||||
|
try {
|
||||||
|
pubDate = LocalDateTime.parse(pubDateStr.trim(), RBI_DATE_FORMAT);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("RBI FeedFetchService - Could not parse date '{}' for: {}", pubDateStr, title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RbiFeedItem feedItem = new RbiFeedItem().setTitle(title).setDescription(description).setLink(link).setPubDate(pubDate)
|
||||||
|
.setCategory(category).setFetchedOn(now);
|
||||||
|
newItems.add(feedItem);
|
||||||
|
log.debug("RBI FeedFetchService - New item queued: {}", title);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newItems.isEmpty()) {
|
||||||
|
repository.saveAll(newItems);
|
||||||
|
log.info("RBI FeedFetchService - Stored {} new items for category: {}", newItems.size(), category);
|
||||||
|
|
||||||
|
// Create OpenProject work packages for NOTIFICATION items only
|
||||||
|
if (NOTIFICATION_CATEGORY.equals(category) && openProjectProperties.isEnabled()) {
|
||||||
|
createOpenProjectWorkPackages(newItems);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("RBI FeedFetchService - No new items for category: {}", category);
|
||||||
|
}
|
||||||
|
return newItems.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates OpenProject work packages for newly fetched NOTIFICATION feed items.
|
||||||
|
* Links each feed item to its work package via openProjectWpId.
|
||||||
|
* Uniqueness is guaranteed because this method is only called for items that
|
||||||
|
* just passed the existsByLink check (new items only).
|
||||||
|
*/
|
||||||
|
private void createOpenProjectWorkPackages(final List<RbiFeedItem> newNotificationItems) {
|
||||||
|
log.info("RBI FeedFetchService - Creating {} OpenProject work packages for NOTIFICATION items", newNotificationItems.size());
|
||||||
|
for (RbiFeedItem feedItem : newNotificationItems) {
|
||||||
|
try {
|
||||||
|
String wpDescription = buildWorkPackageDescription(feedItem);
|
||||||
|
OpenProjectWorkPackageData wp = openProjectService.createWorkPackage(
|
||||||
|
"[RBI Notification] " + feedItem.getTitle(),
|
||||||
|
wpDescription,
|
||||||
|
"Task"
|
||||||
|
);
|
||||||
|
feedItem.setOpenProjectWpId(wp.getId());
|
||||||
|
feedItem.setOpenProjectWpStatus(wp.getStatusName());
|
||||||
|
feedItem.setOpenProjectWpProgress(wp.getPercentageDone());
|
||||||
|
repository.save(feedItem);
|
||||||
|
log.info("RBI FeedFetchService - Created OpenProject WP #{} for feed item: {}", wp.getId(), feedItem.getTitle());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("RBI FeedFetchService - Failed to create OpenProject WP for: {}", feedItem.getTitle(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildWorkPackageDescription(final RbiFeedItem feedItem) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("## RBI Notification\n\n");
|
||||||
|
sb.append("**Title:** ").append(feedItem.getTitle()).append("\n\n");
|
||||||
|
if (feedItem.getDescription() != null && !feedItem.getDescription().isBlank()) {
|
||||||
|
sb.append("**Description:** ").append(feedItem.getDescription()).append("\n\n");
|
||||||
|
}
|
||||||
|
if (feedItem.getPubDate() != null) {
|
||||||
|
sb.append("**Published:** ").append(feedItem.getPubDate().toLocalDate()).append("\n\n");
|
||||||
|
}
|
||||||
|
sb.append("**RBI Link:** ").append(feedItem.getLink()).append("\n\n");
|
||||||
|
sb.append("---\n*Auto-created by CBS Fineract RBI Feed Integration*");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs OpenProject work package status/progress back to linked RBI feed items.
|
||||||
|
* Call this to update progress tracking for all linked NOTIFICATION items.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public int syncOpenProjectStatus() {
|
||||||
|
if (!openProjectProperties.isEnabled()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
List<RbiFeedItem> linkedItems = repository.findByCategoryAndOpenProjectWpIdIsNotNull(NOTIFICATION_CATEGORY);
|
||||||
|
log.info("RBI FeedFetchService - Syncing OpenProject status for {} linked items", linkedItems.size());
|
||||||
|
int updated = 0;
|
||||||
|
for (RbiFeedItem feedItem : linkedItems) {
|
||||||
|
try {
|
||||||
|
var allWps = openProjectService.getAllWorkPackages();
|
||||||
|
for (var wp : allWps) {
|
||||||
|
if (wp.getId().equals(feedItem.getOpenProjectWpId())) {
|
||||||
|
boolean changed = false;
|
||||||
|
if (wp.getStatusName() != null && !wp.getStatusName().equals(feedItem.getOpenProjectWpStatus())) {
|
||||||
|
feedItem.setOpenProjectWpStatus(wp.getStatusName());
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (wp.getPercentageDone() != null && !wp.getPercentageDone().equals(feedItem.getOpenProjectWpProgress())) {
|
||||||
|
feedItem.setOpenProjectWpProgress(wp.getPercentageDone());
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
repository.save(feedItem);
|
||||||
|
updated++;
|
||||||
|
log.debug("RBI FeedFetchService - Updated WP #{} status={} progress={}%",
|
||||||
|
wp.getId(), wp.getStatusName(), wp.getPercentageDone());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("RBI FeedFetchService - Failed to sync status for WP #{}", feedItem.getOpenProjectWpId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("RBI FeedFetchService - Synced {} items", updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getElementText(Element parent, String tagName) {
|
||||||
|
NodeList nodes = parent.getElementsByTagName(tagName);
|
||||||
|
if (nodes.getLength() > 0) {
|
||||||
|
return nodes.item(0).getTextContent();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -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.infrastructure.rbifeed.service;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import org.apache.fineract.infrastructure.rbifeed.data.RbiFeedItemData;
|
||||||
|
|
||||||
|
public interface RbiFeedReadPlatformService {
|
||||||
|
|
||||||
|
Collection<RbiFeedItemData> retrieveAll();
|
||||||
|
|
||||||
|
Collection<RbiFeedItemData> retrieveByCategory(String category);
|
||||||
|
|
||||||
|
RbiFeedItemData retrieveOne(Long feedItemId);
|
||||||
|
}
|
||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.infrastructure.rbifeed.service;
|
||||||
|
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Collection;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.infrastructure.rbifeed.data.RbiFeedItemData;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.jdbc.core.RowMapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class RbiFeedReadPlatformServiceImpl implements RbiFeedReadPlatformService {
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
private static final class RbiFeedItemRowMapper implements RowMapper<RbiFeedItemData> {
|
||||||
|
|
||||||
|
public String schema() {
|
||||||
|
return "f.id, f.title, f.description, f.link, f.pub_date, f.category, f.fetched_on,"
|
||||||
|
+ " f.openproject_wp_id, f.openproject_wp_status, f.openproject_wp_progress"
|
||||||
|
+ " from m_rbi_feed_item f";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RbiFeedItemData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
|
||||||
|
final Long id = rs.getLong("id");
|
||||||
|
final String title = rs.getString("title");
|
||||||
|
final String description = rs.getString("description");
|
||||||
|
final String link = rs.getString("link");
|
||||||
|
final Timestamp pubDateTs = rs.getTimestamp("pub_date");
|
||||||
|
final LocalDateTime pubDate = pubDateTs != null ? pubDateTs.toLocalDateTime() : null;
|
||||||
|
final String category = rs.getString("category");
|
||||||
|
final Timestamp fetchedOnTs = rs.getTimestamp("fetched_on");
|
||||||
|
final LocalDateTime fetchedOn = fetchedOnTs != null ? fetchedOnTs.toLocalDateTime() : null;
|
||||||
|
final long wpIdRaw = rs.getLong("openproject_wp_id");
|
||||||
|
final Long openProjectWpId = rs.wasNull() ? null : wpIdRaw;
|
||||||
|
final String openProjectWpStatus = rs.getString("openproject_wp_status");
|
||||||
|
final int wpProgressRaw = rs.getInt("openproject_wp_progress");
|
||||||
|
final Integer openProjectWpProgress = rs.wasNull() ? null : wpProgressRaw;
|
||||||
|
return RbiFeedItemData.instance(id, title, description, link, pubDate, category, fetchedOn,
|
||||||
|
openProjectWpId, openProjectWpStatus, openProjectWpProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<RbiFeedItemData> retrieveAll() {
|
||||||
|
log.info("RBI Feed ReadService - retrieveAll");
|
||||||
|
final RbiFeedItemRowMapper rm = new RbiFeedItemRowMapper();
|
||||||
|
final String sql = "select " + rm.schema() + " order by f.pub_date desc";
|
||||||
|
final Collection<RbiFeedItemData> results = this.jdbcTemplate.query(sql, rm);
|
||||||
|
log.debug("RBI Feed ReadService - retrieveAll returned {} items", results.size());
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<RbiFeedItemData> retrieveByCategory(final String category) {
|
||||||
|
log.info("RBI Feed ReadService - retrieveByCategory: {}", category);
|
||||||
|
final RbiFeedItemRowMapper rm = new RbiFeedItemRowMapper();
|
||||||
|
final String sql = "select " + rm.schema() + " where f.category = ? order by f.pub_date desc";
|
||||||
|
final Collection<RbiFeedItemData> results = this.jdbcTemplate.query(sql, rm, category);
|
||||||
|
log.debug("RBI Feed ReadService - retrieveByCategory returned {} items for {}", results.size(), category);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RbiFeedItemData retrieveOne(final Long feedItemId) {
|
||||||
|
log.info("RBI Feed ReadService - retrieveOne: {}", feedItemId);
|
||||||
|
final RbiFeedItemRowMapper rm = new RbiFeedItemRowMapper();
|
||||||
|
final String sql = "select " + rm.schema() + " where f.id = ?";
|
||||||
|
return this.jdbcTemplate.queryForObject(sql, rm, feedItemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -135,6 +135,10 @@ public class ChargeReadPlatformServiceImpl implements ChargeReadPlatformService
|
|||||||
final List<EnumOptionData> shareChargeCalculationTypeOptions = this.chargeDropdownReadPlatformService
|
final List<EnumOptionData> shareChargeCalculationTypeOptions = this.chargeDropdownReadPlatformService
|
||||||
.retrieveSharesCalculationTypes();
|
.retrieveSharesCalculationTypes();
|
||||||
final List<EnumOptionData> shareChargeTimeTypeOptions = this.chargeDropdownReadPlatformService.retrieveSharesCollectionTimeTypes();
|
final List<EnumOptionData> shareChargeTimeTypeOptions = this.chargeDropdownReadPlatformService.retrieveSharesCollectionTimeTypes();
|
||||||
|
final List<EnumOptionData> currentAccountChargeCalculationTypeOptions = this.chargeDropdownReadPlatformService
|
||||||
|
.retrieveCurrentAccountCalculationTypes();
|
||||||
|
final List<EnumOptionData> currentAccountChargeTimeTypeOptions = this.chargeDropdownReadPlatformService
|
||||||
|
.retrieveCurrentAccountCollectionTimeTypes();
|
||||||
final Collection<TaxGroupData> taxGroupOptions = this.taxReadPlatformService.retrieveTaxGroupsForLookUp();
|
final Collection<TaxGroupData> taxGroupOptions = this.taxReadPlatformService.retrieveTaxGroupsForLookUp();
|
||||||
final String accountMappingForChargeConfig = this.configurationDomainServiceJpa.getAccountMappingForCharge();
|
final String accountMappingForChargeConfig = this.configurationDomainServiceJpa.getAccountMappingForCharge();
|
||||||
final List<GLAccountData> expenseAccountOptions = this.accountingDropdownReadPlatformService.retrieveExpenseAccountOptions();
|
final List<GLAccountData> expenseAccountOptions = this.accountingDropdownReadPlatformService.retrieveExpenseAccountOptions();
|
||||||
@@ -150,6 +154,8 @@ public class ChargeReadPlatformServiceImpl implements ChargeReadPlatformService
|
|||||||
.clientChargeTimeTypeOptions(clientChargeTimeTypeOptions).feeFrequencyOptions(feeFrequencyOptions)
|
.clientChargeTimeTypeOptions(clientChargeTimeTypeOptions).feeFrequencyOptions(feeFrequencyOptions)
|
||||||
.incomeOrLiabilityAccountOptions(incomeOrLiabilityAccountOptions).taxGroupOptions(taxGroupOptions)
|
.incomeOrLiabilityAccountOptions(incomeOrLiabilityAccountOptions).taxGroupOptions(taxGroupOptions)
|
||||||
.shareChargeCalculationTypeOptions(shareChargeCalculationTypeOptions).shareChargeTimeTypeOptions(shareChargeTimeTypeOptions)
|
.shareChargeCalculationTypeOptions(shareChargeCalculationTypeOptions).shareChargeTimeTypeOptions(shareChargeTimeTypeOptions)
|
||||||
|
.currentAccountChargeCalculationTypeOptions(currentAccountChargeCalculationTypeOptions)
|
||||||
|
.currentAccountChargeTimeTypeOptions(currentAccountChargeTimeTypeOptions)
|
||||||
.accountMappingForChargeConfig(accountMappingForChargeConfig).expenseAccountOptions(expenseAccountOptions)
|
.accountMappingForChargeConfig(accountMappingForChargeConfig).expenseAccountOptions(expenseAccountOptions)
|
||||||
.assetAccountOptions(assetAccountOptions).build();
|
.assetAccountOptions(assetAccountOptions).build();
|
||||||
}
|
}
|
||||||
|
|||||||
+267
@@ -0,0 +1,267 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* 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.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.PUT;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
|
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||||
|
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.client.data.ClientKycApiConstants;
|
||||||
|
import org.apache.fineract.portfolio.client.data.ClientKycData;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.apache.fineract.portfolio.client.data.ClientKycRequest;
|
||||||
|
import org.apache.fineract.portfolio.client.service.ClientKycReadPlatformService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST endpoints for client KYC (Know Your Customer) as mandated by RBI Master Direction on KYC.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* POST /v1/clients/{clientId}/kyc – Initiate KYC
|
||||||
|
* GET /v1/clients/{clientId}/kyc – Get KYC record for a client
|
||||||
|
* GET /v1/clients/{clientId}/kyc/{kycId} – Get KYC record by id
|
||||||
|
* PUT /v1/clients/{clientId}/kyc/{kycId} – Update KYC documents
|
||||||
|
* POST /v1/clients/{clientId}/kyc/{kycId}?command=aadhaar-generate-otp – Step 1: send Aadhaar OTP
|
||||||
|
* POST /v1/clients/{clientId}/kyc/{kycId}?command=verify-pan – Verify PAN via LincPay
|
||||||
|
* POST /v1/clients/{clientId}/kyc/{kycId}?command=verify-aadhaar – Step 2: verify Aadhaar OTP
|
||||||
|
* POST /v1/clients/{clientId}/kyc/{kycId}?command=verify-gstin – Verify GSTIN via LincPay
|
||||||
|
* POST /v1/clients/{clientId}/kyc/{kycId}?command=verify – Verify all in one shot
|
||||||
|
* POST /v1/clients/{clientId}/kyc/{kycId}?command=reject – Reject KYC
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Path("/v1/clients/{clientId}/kyc")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Client KYC",
|
||||||
|
description = "Client KYC (Know Your Customer) module compliant with RBI Master Direction on KYC. "
|
||||||
|
+ "Supports PAN, Aadhaar and GSTIN verification via pluggable 3rd-party APIs.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ClientKycApiResource {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ClientKycApiResource.class);
|
||||||
|
private static final String RESOURCE_NAME_FOR_PERMISSIONS = ClientKycApiConstants.RESOURCE_NAME;
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final ClientKycReadPlatformService readPlatformService;
|
||||||
|
private final DefaultToApiJsonSerializer<ClientKycData> toApiJsonSerializer;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// INITIATE KYC – POST /v1/clients/{clientId}/kyc
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(
|
||||||
|
summary = "Initiate KYC for a Client",
|
||||||
|
description = "Creates a new KYC record for the client with status **PENDING**.\n\n"
|
||||||
|
+ "**RBI Requirements:**\n"
|
||||||
|
+ "- PAN is mandatory for all customers.\n"
|
||||||
|
+ "- Aadhaar is mandatory for INDIVIDUAL clients.\n"
|
||||||
|
+ "- GSTIN is mandatory for NON_INDIVIDUAL (business) clients.\n"
|
||||||
|
+ "- Aadhaar must be stored **masked** (first 8 digits replaced with X).")
|
||||||
|
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = ClientKycRequest.class)))
|
||||||
|
@ApiResponse(responseCode = "200", description = "KYC initiated successfully",
|
||||||
|
content = @Content(schema = @Schema(implementation = CommandProcessingResult.class)))
|
||||||
|
@ApiResponse(responseCode = "400", description = "Invalid request / validation error")
|
||||||
|
@ApiResponse(responseCode = "409", description = "KYC already exists for this client")
|
||||||
|
public CommandProcessingResult initiateKyc(
|
||||||
|
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
|
||||||
|
@Parameter(hidden = true) final ClientKycRequest clientKycRequest) {
|
||||||
|
|
||||||
|
final CommandWrapper commandRequest = new CommandWrapperBuilder()
|
||||||
|
.initiateClientKyc(clientId)
|
||||||
|
.withJson(toApiJsonSerializer.serialize(clientKycRequest))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// GET KYC for client – GET /v1/clients/{clientId}/kyc
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve KYC record for a Client",
|
||||||
|
description = "Returns the KYC record associated with the given client.")
|
||||||
|
@ApiResponse(responseCode = "200", description = "OK",
|
||||||
|
content = @Content(schema = @Schema(implementation = ClientKycData.class)))
|
||||||
|
@ApiResponse(responseCode = "404", description = "KYC record not found")
|
||||||
|
public String retrieveClientKycByClientId(
|
||||||
|
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId) {
|
||||||
|
|
||||||
|
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
final ClientKycData data = readPlatformService.retrieveClientKycByClientId(clientId);
|
||||||
|
return toApiJsonSerializer.serialize(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// GET KYC by ID – GET /v1/clients/{clientId}/kyc/{kycId}
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{kycId}")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve a KYC record by ID",
|
||||||
|
description = "Returns the KYC record identified by {kycId} for the given client.")
|
||||||
|
@ApiResponse(responseCode = "200", description = "OK",
|
||||||
|
content = @Content(schema = @Schema(implementation = ClientKycData.class)))
|
||||||
|
public String retrieveClientKyc(
|
||||||
|
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
|
||||||
|
@PathParam("kycId") @Parameter(description = "KYC record identifier") final Long kycId) {
|
||||||
|
|
||||||
|
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
final ClientKycData data = readPlatformService.retrieveClientKyc(kycId);
|
||||||
|
return toApiJsonSerializer.serialize(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// UPDATE KYC – PUT /v1/clients/{clientId}/kyc/{kycId}
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("{kycId}")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(
|
||||||
|
summary = "Update KYC document details",
|
||||||
|
description = "Updates PAN, Aadhaar, GSTIN or risk category on an existing KYC record.\n"
|
||||||
|
+ "Transitions the status to **IN_PROGRESS** if documents are changed.\n"
|
||||||
|
+ "An already **VERIFIED** KYC cannot be updated.")
|
||||||
|
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = ClientKycRequest.class)))
|
||||||
|
@ApiResponse(responseCode = "200", description = "KYC updated",
|
||||||
|
content = @Content(schema = @Schema(implementation = CommandProcessingResult.class)))
|
||||||
|
public CommandProcessingResult updateClientKyc(
|
||||||
|
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
|
||||||
|
@PathParam("kycId") @Parameter(description = "KYC record identifier") final Long kycId,
|
||||||
|
@Parameter(hidden = true) final ClientKycRequest clientKycRequest) {
|
||||||
|
|
||||||
|
final CommandWrapper commandRequest = new CommandWrapperBuilder()
|
||||||
|
.updateClientKyc(clientId, kycId)
|
||||||
|
.withJson(toApiJsonSerializer.serialize(clientKycRequest))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// VERIFY / REJECT – POST /v1/clients/{clientId}/kyc/{kycId}?command=
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{kycId}")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(
|
||||||
|
summary = "Execute a KYC state-change command",
|
||||||
|
description = "Supported commands:\n\n"
|
||||||
|
+ "### Aadhaar e-KYC (2-step)\n"
|
||||||
|
+ "- **aadhaar-generate-otp** – Step 1: send the client's full 12-digit Aadhaar (`aadhaarNumber`) "
|
||||||
|
+ "to trigger an OTP on their registered mobile via LincPay. "
|
||||||
|
+ "The full Aadhaar is **never stored**. "
|
||||||
|
+ "Response includes `aadhaarRefNumber` required for step 2.\n"
|
||||||
|
+ "- **verify-aadhaar** – Step 2: verify the OTP. "
|
||||||
|
+ "Supply `aadhaarRefNumber` (from step 1) and `aadhaarOtp` (entered by client). "
|
||||||
|
+ "Marks `aadhaarVerified = true` on success. KYC is **not** rejected on failure; "
|
||||||
|
+ "the UI can display the error and let the user retry.\n\n"
|
||||||
|
+ "### Individual verifications\n"
|
||||||
|
+ "- **verify-pan** – Validate the stored PAN against Income Tax Dept via LincPay. "
|
||||||
|
+ "No request body needed. Marks `panVerified = true` on success.\n"
|
||||||
|
+ "- **verify-gstin** – Validate the stored GSTIN against the GST Common Portal via LincPay. "
|
||||||
|
+ "No request body needed. Marks `gstinVerified = true` on success.\n\n"
|
||||||
|
+ "### Batch verification (legacy / all-in-one)\n"
|
||||||
|
+ "- **verify** – Trigger one or more verifications in a single call:\n"
|
||||||
|
+ " - `verifyPan=true` → PAN validated.\n"
|
||||||
|
+ " - `verifyAadhaar=true` → Aadhaar OTP verified (requires `aadhaarRefNumber` + `aadhaarOtp`).\n"
|
||||||
|
+ " - `verifyGstin=true` → GSTIN validated.\n"
|
||||||
|
+ " If all pass, status moves to **VERIFIED**. On any failure, KYC is **REJECTED**.\n\n"
|
||||||
|
+ "### Rejection\n"
|
||||||
|
+ "- **reject** – Manually reject the KYC. `remarks` (reason) is mandatory.")
|
||||||
|
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = ClientKycRequest.class)))
|
||||||
|
@ApiResponse(responseCode = "200", description = "Command executed",
|
||||||
|
content = @Content(schema = @Schema(implementation = CommandProcessingResult.class)))
|
||||||
|
@ApiResponse(responseCode = "400", description = "Invalid command or state transition not allowed")
|
||||||
|
public CommandProcessingResult handleKycCommand(
|
||||||
|
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
|
||||||
|
@PathParam("kycId") @Parameter(description = "KYC record identifier") final Long kycId,
|
||||||
|
@QueryParam("command") @Parameter(
|
||||||
|
description = "Command: aadhaar-generate-otp | verify-pan | verify-aadhaar | verify-gstin | verify | reject")
|
||||||
|
final String command,
|
||||||
|
@Parameter(hidden = true) final ClientKycRequest clientKycRequest) {
|
||||||
|
|
||||||
|
final String json = toApiJsonSerializer.serialize(clientKycRequest);
|
||||||
|
|
||||||
|
LOG.info("KYC command received: clientId={}, kycId={}, command={}, requestBody={}", clientId, kycId, command, json);
|
||||||
|
|
||||||
|
final CommandWrapper commandRequest;
|
||||||
|
if ("aadhaar-generate-otp".equalsIgnoreCase(command)) {
|
||||||
|
commandRequest = new CommandWrapperBuilder()
|
||||||
|
.generateAadhaarOtpClientKyc(clientId, kycId).withJson(json).build();
|
||||||
|
} else if ("verify-pan".equalsIgnoreCase(command)) {
|
||||||
|
commandRequest = new CommandWrapperBuilder()
|
||||||
|
.verifyPanClientKyc(clientId, kycId).withJson(json).build();
|
||||||
|
} else if ("verify-aadhaar".equalsIgnoreCase(command)) {
|
||||||
|
commandRequest = new CommandWrapperBuilder()
|
||||||
|
.verifyAadhaarClientKyc(clientId, kycId).withJson(json).build();
|
||||||
|
} else if ("verify-gstin".equalsIgnoreCase(command)) {
|
||||||
|
commandRequest = new CommandWrapperBuilder()
|
||||||
|
.verifyGstinClientKyc(clientId, kycId).withJson(json).build();
|
||||||
|
} else if ("verify".equalsIgnoreCase(command)) {
|
||||||
|
commandRequest = new CommandWrapperBuilder().verifyClientKyc(clientId, kycId).withJson(json).build();
|
||||||
|
} else if ("reject".equalsIgnoreCase(command)) {
|
||||||
|
commandRequest = new CommandWrapperBuilder().rejectClientKyc(clientId, kycId).withJson(json).build();
|
||||||
|
} else {
|
||||||
|
throw new org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException(
|
||||||
|
"command", command,
|
||||||
|
"aadhaar-generate-otp", "verify-pan", "verify-aadhaar", "verify-gstin", "verify", "reject");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||||
|
LOG.info("KYC command '{}' completed successfully for clientId={}, kycId={}", command, clientId, kycId);
|
||||||
|
return result;
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.error("KYC command '{}' failed for clientId={}, kycId={}: {}", command, clientId, kycId, e.getMessage(), e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+220
@@ -0,0 +1,220 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* 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.command;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
|
import org.apache.fineract.portfolio.client.data.ClientKycApiConstants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable command object for KYC operations. Validation is performed at
|
||||||
|
* deserializer stage; this class adds domain-level cross-field checks.
|
||||||
|
*/
|
||||||
|
public class ClientKycCommand {
|
||||||
|
|
||||||
|
private final Integer kycType;
|
||||||
|
private final Integer riskCategory;
|
||||||
|
private final String panNumber;
|
||||||
|
private final String aadhaarNumberMasked;
|
||||||
|
private final String gstinNumber;
|
||||||
|
private final String remarks;
|
||||||
|
private final String ckycNumber;
|
||||||
|
private final Boolean verifyPan;
|
||||||
|
private final Boolean verifyAadhaar;
|
||||||
|
private final Boolean verifyGstin;
|
||||||
|
|
||||||
|
// ── Aadhaar OTP flow ──────────────────────────────────────────────────────
|
||||||
|
/** Full Aadhaar – used ONLY for OTP generation; never stored. */
|
||||||
|
private final String aadhaarNumber;
|
||||||
|
/** refNumber returned by LincPay generate-otp; supplied for verify-otp. */
|
||||||
|
private final String aadhaarRefNumber;
|
||||||
|
/** OTP entered by the client. */
|
||||||
|
private final String aadhaarOtp;
|
||||||
|
|
||||||
|
public ClientKycCommand(final Integer kycType, final Integer riskCategory, final String panNumber,
|
||||||
|
final String aadhaarNumberMasked, final String gstinNumber, final String remarks,
|
||||||
|
final String ckycNumber, final Boolean verifyPan, final Boolean verifyAadhaar,
|
||||||
|
final Boolean verifyGstin, final String aadhaarNumber, final String aadhaarRefNumber,
|
||||||
|
final String aadhaarOtp) {
|
||||||
|
this.kycType = kycType;
|
||||||
|
this.riskCategory = riskCategory;
|
||||||
|
this.panNumber = panNumber;
|
||||||
|
this.aadhaarNumberMasked = aadhaarNumberMasked;
|
||||||
|
this.gstinNumber = gstinNumber;
|
||||||
|
this.remarks = remarks;
|
||||||
|
this.ckycNumber = ckycNumber;
|
||||||
|
this.verifyPan = verifyPan;
|
||||||
|
this.verifyAadhaar = verifyAadhaar;
|
||||||
|
this.verifyGstin = verifyGstin;
|
||||||
|
this.aadhaarNumber = aadhaarNumber;
|
||||||
|
this.aadhaarRefNumber = aadhaarRefNumber;
|
||||||
|
this.aadhaarOtp = aadhaarOtp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateForInitiate() {
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ClientKycApiConstants.RESOURCE_NAME);
|
||||||
|
|
||||||
|
v.reset().parameter(ClientKycApiConstants.kycTypeParam).value(kycType).notNull().inMinMaxRange(1, 2);
|
||||||
|
v.reset().parameter(ClientKycApiConstants.riskCategoryParam).value(riskCategory).notNull().inMinMaxRange(1, 3);
|
||||||
|
|
||||||
|
// PAN is mandatory as per RBI KYC guidelines
|
||||||
|
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber).notBlank()
|
||||||
|
.notExceedingLengthOf(10)
|
||||||
|
.matchesRegularExpression(ClientKycApiConstants.PAN_REGEX,
|
||||||
|
"PAN must be in format: 5 uppercase letters + 4 digits + 1 uppercase letter (e.g. ABCDE1234F)");
|
||||||
|
|
||||||
|
// Aadhaar – mandatory for individual
|
||||||
|
if (kycType != null && kycType == 1) {
|
||||||
|
v.reset().parameter(ClientKycApiConstants.aadhaarNumberMaskedParam).value(aadhaarNumberMasked).notBlank()
|
||||||
|
.notExceedingLengthOf(12)
|
||||||
|
.matchesRegularExpression(ClientKycApiConstants.AADHAAR_MASKED_REGEX,
|
||||||
|
"Aadhaar masked number must be 12 characters (first 8 masked, last 4 digits visible)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// GSTIN – mandatory for non-individual
|
||||||
|
if (kycType != null && kycType == 2) {
|
||||||
|
v.reset().parameter(ClientKycApiConstants.gstinNumberParam).value(gstinNumber).notBlank()
|
||||||
|
.notExceedingLengthOf(15)
|
||||||
|
.matchesRegularExpression(ClientKycApiConstants.GSTIN_REGEX,
|
||||||
|
"GSTIN must be a valid 15-character GST Identification Number");
|
||||||
|
}
|
||||||
|
|
||||||
|
throwExceptionIfValidationWarningsExist(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateForUpdate() {
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ClientKycApiConstants.RESOURCE_NAME);
|
||||||
|
|
||||||
|
if (panNumber != null) {
|
||||||
|
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber).notBlank()
|
||||||
|
.notExceedingLengthOf(10)
|
||||||
|
.matchesRegularExpression(ClientKycApiConstants.PAN_REGEX,
|
||||||
|
"PAN must be in format: 5 uppercase letters + 4 digits + 1 uppercase letter");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aadhaarNumberMasked != null) {
|
||||||
|
v.reset().parameter(ClientKycApiConstants.aadhaarNumberMaskedParam).value(aadhaarNumberMasked)
|
||||||
|
.notExceedingLengthOf(12)
|
||||||
|
.matchesRegularExpression(ClientKycApiConstants.AADHAAR_MASKED_REGEX,
|
||||||
|
"Aadhaar masked number must be 12 characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gstinNumber != null) {
|
||||||
|
v.reset().parameter(ClientKycApiConstants.gstinNumberParam).value(gstinNumber)
|
||||||
|
.notExceedingLengthOf(15)
|
||||||
|
.matchesRegularExpression(ClientKycApiConstants.GSTIN_REGEX,
|
||||||
|
"GSTIN must be a valid 15-character GST Identification Number");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (riskCategory != null) {
|
||||||
|
v.reset().parameter(ClientKycApiConstants.riskCategoryParam).value(riskCategory).inMinMaxRange(1, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
throwExceptionIfValidationWarningsExist(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateForAadhaarGenerateOtp() {
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ClientKycApiConstants.RESOURCE_NAME);
|
||||||
|
|
||||||
|
v.reset().parameter(ClientKycApiConstants.aadhaarNumberParam).value(aadhaarNumber).notBlank()
|
||||||
|
.notExceedingLengthOf(12)
|
||||||
|
.matchesRegularExpression("^[0-9]{12}$",
|
||||||
|
"Aadhaar number must be exactly 12 digits");
|
||||||
|
|
||||||
|
throwExceptionIfValidationWarningsExist(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateForVerify() {
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ClientKycApiConstants.RESOURCE_NAME);
|
||||||
|
|
||||||
|
if (ckycNumber != null) {
|
||||||
|
v.reset().parameter(ClientKycApiConstants.ckycNumberParam).value(ckycNumber).notExceedingLengthOf(14);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If verifyAadhaar=true, refNumber and OTP are mandatory
|
||||||
|
if (Boolean.TRUE.equals(verifyAadhaar)) {
|
||||||
|
v.reset().parameter(ClientKycApiConstants.aadhaarRefNumberParam).value(aadhaarRefNumber).notBlank();
|
||||||
|
v.reset().parameter(ClientKycApiConstants.aadhaarOtpParam).value(aadhaarOtp).notBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
throwExceptionIfValidationWarningsExist(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** verify-pan command: PAN is read from the existing KYC record – no required body fields. */
|
||||||
|
public void validateForVerifyPan() {
|
||||||
|
// PAN is already stored on the KYC entity; nothing to validate in the command body.
|
||||||
|
}
|
||||||
|
|
||||||
|
/** verify-aadhaar command: refNumber and OTP are required. */
|
||||||
|
public void validateForVerifyAadhaar() {
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ClientKycApiConstants.RESOURCE_NAME);
|
||||||
|
|
||||||
|
v.reset().parameter(ClientKycApiConstants.aadhaarRefNumberParam).value(aadhaarRefNumber).notBlank();
|
||||||
|
v.reset().parameter(ClientKycApiConstants.aadhaarOtpParam).value(aadhaarOtp).notBlank();
|
||||||
|
|
||||||
|
throwExceptionIfValidationWarningsExist(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** verify-gstin command: GSTIN is read from the existing KYC record – no required body fields. */
|
||||||
|
public void validateForVerifyGstin() {
|
||||||
|
// GSTIN is already stored on the KYC entity; nothing to validate in the command body.
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateForReject() {
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ClientKycApiConstants.RESOURCE_NAME);
|
||||||
|
|
||||||
|
v.reset().parameter(ClientKycApiConstants.remarksParam).value(remarks).notBlank().notExceedingLengthOf(500);
|
||||||
|
|
||||||
|
throwExceptionIfValidationWarningsExist(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> errors) {
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
throw new PlatformApiDataValidationException(errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Accessors ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public Integer getKycType() { return kycType; }
|
||||||
|
public Integer getRiskCategory() { return riskCategory; }
|
||||||
|
public String getPanNumber() { return panNumber; }
|
||||||
|
public String getAadhaarNumberMasked() { return aadhaarNumberMasked; }
|
||||||
|
public String getGstinNumber() { return gstinNumber; }
|
||||||
|
public String getRemarks() { return remarks; }
|
||||||
|
public String getCkycNumber() { return ckycNumber; }
|
||||||
|
public Boolean getVerifyPan() { return verifyPan; }
|
||||||
|
public Boolean getVerifyAadhaar() { return verifyAadhaar; }
|
||||||
|
public Boolean getVerifyGstin() { return verifyGstin; }
|
||||||
|
/** Full Aadhaar – used ONLY for OTP generation; never stored. */
|
||||||
|
public String getAadhaarNumber() { return aadhaarNumber; }
|
||||||
|
/** refNumber returned by LincPay generate-otp. */
|
||||||
|
public String getAadhaarRefNumber() { return aadhaarRefNumber; }
|
||||||
|
/** OTP entered by the client. */
|
||||||
|
public String getAadhaarOtp() { return aadhaarOtp; }
|
||||||
|
}
|
||||||
+1
-1
@@ -30,7 +30,7 @@ public class ClientApiCollectionConstants extends ClientApiConstants {
|
|||||||
emailAddressParamName, firstnameParamName, middlenameParamName, lastnameParamName, fullnameParamName, officeIdParamName,
|
emailAddressParamName, firstnameParamName, middlenameParamName, lastnameParamName, fullnameParamName, officeIdParamName,
|
||||||
activeParamName, activationDateParamName, staffIdParamName, submittedOnDateParamName, savingsProductIdParamName,
|
activeParamName, activationDateParamName, staffIdParamName, submittedOnDateParamName, savingsProductIdParamName,
|
||||||
dateOfBirthParamName, genderIdParamName, clientTypeIdParamName, clientClassificationIdParamName,
|
dateOfBirthParamName, genderIdParamName, clientTypeIdParamName, clientClassificationIdParamName,
|
||||||
clientNonPersonDetailsParamName, displaynameParamName, legalFormIdParamName, datatables, isStaffParamName));
|
clientNonPersonDetailsParamName, displaynameParamName, legalFormIdParamName, datatables, isStaffParamName, kycParamName));
|
||||||
|
|
||||||
protected static final Set<String> CLIENT_NON_PERSON_CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
protected static final Set<String> CLIENT_NON_PERSON_CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||||
Arrays.asList(familyMembers, address, localeParamName, dateFormatParamName, incorpNumberParamName, remarksParamName,
|
Arrays.asList(familyMembers, address, localeParamName, dateFormatParamName, incorpNumberParamName, remarksParamName,
|
||||||
|
|||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.data;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class ClientKycApiConstants {
|
||||||
|
|
||||||
|
private ClientKycApiConstants() {}
|
||||||
|
|
||||||
|
public static final String RESOURCE_NAME = "CLIENTKYC";
|
||||||
|
|
||||||
|
// ── Request parameters ────────────────────────────────────────────────────
|
||||||
|
public static final String kycTypeParam = "kycType";
|
||||||
|
public static final String riskCategoryParam = "riskCategory";
|
||||||
|
public static final String panNumberParam = "panNumber";
|
||||||
|
public static final String aadhaarNumberMaskedParam = "aadhaarNumberMasked";
|
||||||
|
public static final String gstinNumberParam = "gstinNumber";
|
||||||
|
public static final String remarksParam = "remarks";
|
||||||
|
public static final String ckycNumberParam = "ckycNumber";
|
||||||
|
public static final String verifyPanParam = "verifyPan";
|
||||||
|
public static final String verifyAadhaarParam = "verifyAadhaar";
|
||||||
|
public static final String verifyGstinParam = "verifyGstin";
|
||||||
|
|
||||||
|
// ── Aadhaar OTP flow parameters ───────────────────────────────────────────
|
||||||
|
/** Full 12-digit Aadhaar – used ONLY for OTP generation; never stored per UIDAI directive. */
|
||||||
|
public static final String aadhaarNumberParam = "aadhaarNumber";
|
||||||
|
/** refNumber returned by LincPay generate-otp; supplied back with the OTP for verify-otp. */
|
||||||
|
public static final String aadhaarRefNumberParam = "aadhaarRefNumber";
|
||||||
|
/** One-time password entered by the client – required for the verify-otp step. */
|
||||||
|
public static final String aadhaarOtpParam = "aadhaarOtp";
|
||||||
|
|
||||||
|
// ── Supported parameters for initiate/update ──────────────────────────────
|
||||||
|
public static final Set<String> INITIATE_REQUEST_PARAMS = new HashSet<>(Arrays.asList(
|
||||||
|
kycTypeParam, riskCategoryParam, panNumberParam, aadhaarNumberMaskedParam,
|
||||||
|
gstinNumberParam, remarksParam, "locale"));
|
||||||
|
|
||||||
|
public static final Set<String> UPDATE_REQUEST_PARAMS = new HashSet<>(Arrays.asList(
|
||||||
|
panNumberParam, aadhaarNumberMaskedParam, gstinNumberParam,
|
||||||
|
riskCategoryParam, remarksParam, "locale"));
|
||||||
|
|
||||||
|
/** verify command: existing flags + Aadhaar OTP fields for the verify-otp step. */
|
||||||
|
public static final Set<String> VERIFY_REQUEST_PARAMS = new HashSet<>(Arrays.asList(
|
||||||
|
ckycNumberParam, remarksParam, verifyPanParam, verifyAadhaarParam, verifyGstinParam,
|
||||||
|
aadhaarRefNumberParam, aadhaarOtpParam));
|
||||||
|
|
||||||
|
/** aadhaar-generate-otp command: only requires the client's full Aadhaar number. */
|
||||||
|
public static final Set<String> AADHAAR_GENERATE_OTP_REQUEST_PARAMS = new HashSet<>(
|
||||||
|
Arrays.asList(aadhaarNumberParam));
|
||||||
|
|
||||||
|
/** verify-pan command: PAN is already stored; no extra body params required. */
|
||||||
|
public static final Set<String> VERIFY_PAN_REQUEST_PARAMS = new HashSet<>();
|
||||||
|
|
||||||
|
/** verify-aadhaar command: requires the refNumber from the generate-otp step and the OTP entered by the client. */
|
||||||
|
public static final Set<String> VERIFY_AADHAAR_REQUEST_PARAMS = new HashSet<>(
|
||||||
|
Arrays.asList(aadhaarRefNumberParam, aadhaarOtpParam));
|
||||||
|
|
||||||
|
/** verify-gstin command: GSTIN is already stored; no extra body params required. */
|
||||||
|
public static final Set<String> VERIFY_GSTIN_REQUEST_PARAMS = new HashSet<>();
|
||||||
|
|
||||||
|
public static final Set<String> REJECT_REQUEST_PARAMS = new HashSet<>(Arrays.asList(remarksParam));
|
||||||
|
|
||||||
|
// ── PAN regex: 5 uppercase letters + 4 digits + 1 uppercase letter ────────
|
||||||
|
public static final String PAN_REGEX = "^[A-Z]{5}[0-9]{4}[A-Z]{1}$";
|
||||||
|
|
||||||
|
// ── Aadhaar masked: stored as XXXX-XXXX-NNNN or 12 digits ────────────────
|
||||||
|
public static final String AADHAAR_MASKED_REGEX = "^[Xx0-9]{8}[0-9]{4}$";
|
||||||
|
|
||||||
|
// ── GSTIN: 2-digit state + 10-char PAN + 1 entity + Z + check digit ──────
|
||||||
|
public static final String GSTIN_REGEX = "^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$";
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only DTO returned by the KYC read-service and serialised to JSON by the API layer.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public final class ClientKycData {
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final Long clientId;
|
||||||
|
private final String clientDisplayName;
|
||||||
|
|
||||||
|
// ── Status & type ─────────────────────────────────────────────────────────
|
||||||
|
private final Integer kycStatus;
|
||||||
|
private final String kycStatusCode;
|
||||||
|
private final Integer kycType;
|
||||||
|
private final String kycTypeCode;
|
||||||
|
private final Integer riskCategory;
|
||||||
|
private final String riskCategoryCode;
|
||||||
|
|
||||||
|
// ── PAN ───────────────────────────────────────────────────────────────────
|
||||||
|
private final String panNumber;
|
||||||
|
private final Boolean panVerified;
|
||||||
|
private final LocalDate panVerifiedOn;
|
||||||
|
private final String panFullName;
|
||||||
|
private final String panAadhaarSeedingStatus;
|
||||||
|
private final String panCategory;
|
||||||
|
private final String panStatus;
|
||||||
|
|
||||||
|
// ── Aadhaar ───────────────────────────────────────────────────────────────
|
||||||
|
private final String aadhaarNumberMasked;
|
||||||
|
private final Boolean aadhaarVerified;
|
||||||
|
private final LocalDate aadhaarVerifiedOn;
|
||||||
|
|
||||||
|
// ── GSTIN ─────────────────────────────────────────────────────────────────
|
||||||
|
private final String gstinNumber;
|
||||||
|
private final Boolean gstinVerified;
|
||||||
|
private final LocalDate gstinVerifiedOn;
|
||||||
|
|
||||||
|
// ── CKYCRR ────────────────────────────────────────────────────────────────
|
||||||
|
private final String ckycNumber;
|
||||||
|
|
||||||
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
private final LocalDate initiatedOn;
|
||||||
|
private final LocalDate verifiedOn;
|
||||||
|
private final LocalDate rejectedOn;
|
||||||
|
private final LocalDate expiryDate;
|
||||||
|
private final String remarks;
|
||||||
|
}
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.data;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inbound request body DTO for KYC initiation and update endpoints.
|
||||||
|
* Serialised from JSON by the JAX-RS layer and then forwarded via {@link org.apache.fineract.commands.service.CommandWrapperBuilder}.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ClientKycRequest {
|
||||||
|
|
||||||
|
@Schema(description = "Locale for numeric parameter parsing (e.g. 'en')", example = "en", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String locale;
|
||||||
|
|
||||||
|
@Schema(description = "KYC type: 1 = INDIVIDUAL, 2 = NON_INDIVIDUAL", example = "1", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Integer kycType;
|
||||||
|
|
||||||
|
@Schema(description = "Risk category: 1 = LOW (re-KYC 10yr), 2 = MEDIUM (8yr), 3 = HIGH (2yr)", example = "1")
|
||||||
|
private Integer riskCategory;
|
||||||
|
|
||||||
|
@Schema(description = "PAN number in format AAAAA0000A – mandatory per RBI KYC norms", example = "ABCDE1234F",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String panNumber;
|
||||||
|
|
||||||
|
@Schema(description = "Aadhaar number – last 4 digits visible, first 8 masked (e.g. XXXXXXXX1234). "
|
||||||
|
+ "Full Aadhaar must NOT be stored per UIDAI directive.", example = "XXXXXXXX1234")
|
||||||
|
private String aadhaarNumberMasked;
|
||||||
|
|
||||||
|
@Schema(description = "15-character GSTIN – mandatory for NON_INDIVIDUAL clients", example = "27ABCDE1234F1Z5")
|
||||||
|
private String gstinNumber;
|
||||||
|
|
||||||
|
@Schema(description = "Optional remarks / notes", example = "Documents submitted at branch")
|
||||||
|
private String remarks;
|
||||||
|
|
||||||
|
// ── Verify action only ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Schema(description = "Central KYC Records Registry (CKYCRR) number issued by CERSAI (14 characters)", example = "12345678901234")
|
||||||
|
private String ckycNumber;
|
||||||
|
|
||||||
|
@Schema(description = "Trigger PAN verification via LincPay API", example = "true")
|
||||||
|
private Boolean verifyPan;
|
||||||
|
|
||||||
|
@Schema(description = "Trigger Aadhaar OTP verification via LincPay API. "
|
||||||
|
+ "Requires aadhaarRefNumber (from aadhaar-generate-otp) and aadhaarOtp.", example = "true")
|
||||||
|
private Boolean verifyAadhaar;
|
||||||
|
|
||||||
|
@Schema(description = "Trigger GSTIN verification via LincPay API", example = "false")
|
||||||
|
private Boolean verifyGstin;
|
||||||
|
|
||||||
|
// ── Aadhaar OTP flow (two-step) ───────────────────────────────────────────
|
||||||
|
|
||||||
|
@Schema(description = "Full 12-digit Aadhaar number. Used ONLY for the aadhaar-generate-otp command. "
|
||||||
|
+ "Never stored by the system per UIDAI directive.", example = "123456789012")
|
||||||
|
private String aadhaarNumber;
|
||||||
|
|
||||||
|
@Schema(description = "Reference number returned by the aadhaar-generate-otp command "
|
||||||
|
+ "(LincPay refNumber). Supply this along with aadhaarOtp in the verify command "
|
||||||
|
+ "when verifyAadhaar=true.", example = "LPY-20260226-ABCDE")
|
||||||
|
private String aadhaarRefNumber;
|
||||||
|
|
||||||
|
@Schema(description = "OTP received by the client on their Aadhaar-registered mobile. "
|
||||||
|
+ "Required in the verify command when verifyAadhaar=true.", example = "123456")
|
||||||
|
private String aadhaarOtp;
|
||||||
|
}
|
||||||
+42
-3
@@ -24,6 +24,7 @@ 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 java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
@@ -76,9 +77,19 @@ public class ClientFamilyMembers extends AbstractPersistableCustom<Long> {
|
|||||||
@Column(name = "date_of_birth", nullable = true)
|
@Column(name = "date_of_birth", nullable = true)
|
||||||
private LocalDate dateOfBirth;
|
private LocalDate dateOfBirth;
|
||||||
|
|
||||||
|
@Column(name = "nomination_type")
|
||||||
|
private String nominationType;
|
||||||
|
|
||||||
|
@Column(name = "percentage_share")
|
||||||
|
private BigDecimal percentageShare;
|
||||||
|
|
||||||
|
@Column(name = "remarks")
|
||||||
|
private String remarks;
|
||||||
|
|
||||||
private ClientFamilyMembers(final Client client, final String firstName, final String middleName, final String lastName,
|
private ClientFamilyMembers(final Client client, final String firstName, final String middleName, final String lastName,
|
||||||
final String qualification, final String mobileNumber, final Long age, final Boolean isDependent, final CodeValue relationship,
|
final String qualification, final String mobileNumber, final Long age, final Boolean isDependent, final CodeValue relationship,
|
||||||
final CodeValue maritalStatus, final CodeValue gender, final LocalDate dateOfBirth, final CodeValue profession) {
|
final CodeValue maritalStatus, final CodeValue gender, final LocalDate dateOfBirth, final CodeValue profession,
|
||||||
|
final String nominationType, final BigDecimal percentageShare, final String remarks) {
|
||||||
|
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.firstName = firstName;
|
this.firstName = firstName;
|
||||||
@@ -93,6 +104,9 @@ public class ClientFamilyMembers extends AbstractPersistableCustom<Long> {
|
|||||||
this.gender = gender;
|
this.gender = gender;
|
||||||
this.dateOfBirth = dateOfBirth;
|
this.dateOfBirth = dateOfBirth;
|
||||||
this.profession = profession;
|
this.profession = profession;
|
||||||
|
this.nominationType = nominationType;
|
||||||
|
this.percentageShare = percentageShare;
|
||||||
|
this.remarks = remarks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClientFamilyMembers() {
|
public ClientFamilyMembers() {
|
||||||
@@ -101,9 +115,10 @@ public class ClientFamilyMembers extends AbstractPersistableCustom<Long> {
|
|||||||
|
|
||||||
public static ClientFamilyMembers fromJson(final Client client, final String firstName, final String middleName, final String lastName,
|
public static ClientFamilyMembers fromJson(final Client client, final String firstName, final String middleName, final String lastName,
|
||||||
final String qualification, final String mobileNumber, final Long age, final Boolean isDependent, final CodeValue relationship,
|
final String qualification, final String mobileNumber, final Long age, final Boolean isDependent, final CodeValue relationship,
|
||||||
final CodeValue maritalStatus, final CodeValue gender, final LocalDate dateOfBirth, final CodeValue profession) {
|
final CodeValue maritalStatus, final CodeValue gender, final LocalDate dateOfBirth, final CodeValue profession,
|
||||||
|
final String nominationType, final BigDecimal percentageShare, final String remarks) {
|
||||||
return new ClientFamilyMembers(client, firstName, middleName, lastName, qualification, mobileNumber, age, isDependent, relationship,
|
return new ClientFamilyMembers(client, firstName, middleName, lastName, qualification, mobileNumber, age, isDependent, relationship,
|
||||||
maritalStatus, gender, dateOfBirth, profession);
|
maritalStatus, gender, dateOfBirth, profession, nominationType, percentageShare, remarks);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Client getClient() {
|
public Client getClient() {
|
||||||
@@ -210,4 +225,28 @@ public class ClientFamilyMembers extends AbstractPersistableCustom<Long> {
|
|||||||
this.isDependent = isDependent;
|
this.isDependent = isDependent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getNominationType() {
|
||||||
|
return this.nominationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNominationType(String nominationType) {
|
||||||
|
this.nominationType = nominationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getPercentageShare() {
|
||||||
|
return this.percentageShare;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPercentageShare(BigDecimal percentageShare) {
|
||||||
|
this.percentageShare = percentageShare;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRemarks() {
|
||||||
|
return this.remarks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemarks(String remarks) {
|
||||||
|
this.remarks = remarks;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* 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.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.client.service.ClientKycWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the {@code aadhaar-generate-otp} KYC command.
|
||||||
|
*
|
||||||
|
* <p>Calls LincPay {@code /aadhaar/generate-otp} with the client's full Aadhaar number
|
||||||
|
* (supplied in the request body as {@code aadhaarNumber}). The full Aadhaar is never
|
||||||
|
* stored. The returned {@link CommandProcessingResult} carries a {@code "aadhaarRefNumber"}
|
||||||
|
* change entry that the calling application must pass back to the client so they can supply
|
||||||
|
* it together with their OTP in the subsequent verify command.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "CLIENTKYC", action = "GENERATEAADHAROTP")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GenerateAadhaarOtpClientKycCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final ClientKycWritePlatformService clientKycWritePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return clientKycWritePlatformService.generateAadhaarOtp(
|
||||||
|
command.getClientId(), command.entityId(), command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.client.service.ClientKycWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "CLIENTKYC", action = "CREATE")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InitiateClientKycCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final ClientKycWritePlatformService clientKycWritePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return clientKycWritePlatformService.initiateKyc(command.getClientId(), command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.client.service.ClientKycWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "CLIENTKYC", action = "REJECT")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RejectClientKycCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final ClientKycWritePlatformService clientKycWritePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return clientKycWritePlatformService.rejectKyc(command.getClientId(), command.entityId(), command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.client.service.ClientKycWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "CLIENTKYC", action = "UPDATE")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UpdateClientKycCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final ClientKycWritePlatformService clientKycWritePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return clientKycWritePlatformService.updateKyc(command.getClientId(), command.entityId(), command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.client.service.ClientKycWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the {@code verify-aadhaar} KYC command (Step 2 of Aadhaar e-KYC).
|
||||||
|
*
|
||||||
|
* <p>Calls LincPay {@code /aadhaar/verify-otp} with the {@code aadhaarRefNumber} returned
|
||||||
|
* by the {@code aadhaar-generate-otp} step and the {@code aadhaarOtp} entered by the client.
|
||||||
|
* On success, marks {@code aadhaarVerified = true} on the KYC record.
|
||||||
|
* The full Aadhaar number is never stored per UIDAI directive.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "CLIENTKYC", action = "VERIFYAADHAAR")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class VerifyAadhaarClientKycCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final ClientKycWritePlatformService clientKycWritePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return clientKycWritePlatformService.verifyAadhaar(
|
||||||
|
command.getClientId(), command.entityId(), command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.client.service.ClientKycWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "CLIENTKYC", action = "VERIFY")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class VerifyClientKycCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final ClientKycWritePlatformService clientKycWritePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return clientKycWritePlatformService.verifyKyc(command.getClientId(), command.entityId(), command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.client.service.ClientKycWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the {@code verify-gstin} KYC command.
|
||||||
|
*
|
||||||
|
* <p>Calls LincPay {@code /validate-gstn} with the GSTIN stored on the KYC record.
|
||||||
|
* On success, marks {@code gstinVerified = true} on the KYC record without changing
|
||||||
|
* the overall KYC status.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "CLIENTKYC", action = "VERIFYGSTIN")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class VerifyGstinClientKycCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final ClientKycWritePlatformService clientKycWritePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return clientKycWritePlatformService.verifyGstin(
|
||||||
|
command.getClientId(), command.entityId(), command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.client.service.ClientKycWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the {@code verify-pan} KYC command.
|
||||||
|
*
|
||||||
|
* <p>Calls LincPay {@code /validate-pan} with the PAN stored on the KYC record.
|
||||||
|
* On success, marks {@code panVerified = true} on the KYC record without changing
|
||||||
|
* the overall KYC status (allowing individual document verifications to proceed
|
||||||
|
* independently before a final status transition).
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "CLIENTKYC", action = "VERIFYPAN")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class VerifyPanClientKycCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final ClientKycWritePlatformService clientKycWritePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return clientKycWritePlatformService.verifyPan(
|
||||||
|
command.getClientId(), command.entityId(), command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* 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.kyc;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures the result of a 3rd-party KYC document verification call.
|
||||||
|
* The actual API response payload is kept as a raw string so it can be
|
||||||
|
* stored verbatim in the audit log ({@code m_client_kyc_verification_log}).
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class KycVerificationResult {
|
||||||
|
|
||||||
|
public enum Status {
|
||||||
|
SUCCESS, FAILURE, PENDING, ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Status status;
|
||||||
|
private final String referenceId;
|
||||||
|
private final String rawResponse;
|
||||||
|
private final String errorMessage;
|
||||||
|
|
||||||
|
/** Convenience factory for a successful verification. */
|
||||||
|
public static KycVerificationResult success(final String referenceId, final String rawResponse) {
|
||||||
|
return KycVerificationResult.builder()
|
||||||
|
.status(Status.SUCCESS)
|
||||||
|
.referenceId(referenceId)
|
||||||
|
.rawResponse(rawResponse)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience factory for a failed verification. */
|
||||||
|
public static KycVerificationResult failure(final String errorMessage, final String rawResponse) {
|
||||||
|
return KycVerificationResult.builder()
|
||||||
|
.status(Status.FAILURE)
|
||||||
|
.errorMessage(errorMessage)
|
||||||
|
.rawResponse(rawResponse)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return Status.SUCCESS == this.status;
|
||||||
|
}
|
||||||
|
}
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* 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.kyc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pluggable interface for 3rd-party KYC document verification.
|
||||||
|
*
|
||||||
|
* <p>The active implementation is {@code LincPayKycVerificationServiceImpl} which calls
|
||||||
|
* the LincPay eKYC API ({@code https://ekyc.lincpay.in/EKYC/ekyc/}).
|
||||||
|
* Credentials are configured via {@code fineract.kyc.lincpay.*} in
|
||||||
|
* {@code application.properties}.
|
||||||
|
*
|
||||||
|
* <p>Aadhaar verification is a two-step OTP flow:
|
||||||
|
* <ol>
|
||||||
|
* <li>Call {@link #generateAadhaarOtp(String)} with the client's full Aadhaar – returns a
|
||||||
|
* {@code refNumber} that must be passed back to the client-facing app.</li>
|
||||||
|
* <li>Call {@link #verifyAadhaar(String, String)} with the {@code refNumber} and the OTP
|
||||||
|
* that the client received on their registered mobile.</li>
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
public interface KycVerificationService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 1 of Aadhaar e-KYC: generate an OTP to the client's UIDAI-registered mobile.
|
||||||
|
*
|
||||||
|
* <p><strong>IMPORTANT:</strong> The full Aadhaar number must NOT be stored after this call
|
||||||
|
* per UIDAI directive. Only the masked Aadhaar (last 4 digits) may be persisted.
|
||||||
|
*
|
||||||
|
* @param aadhaarNumber Full 12-digit Aadhaar number
|
||||||
|
* @return result whose {@link KycVerificationResult#getReferenceId()} contains the
|
||||||
|
* {@code refNumber} required for the subsequent OTP verification call
|
||||||
|
*/
|
||||||
|
KycVerificationResult generateAadhaarOtp(String aadhaarNumber);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 2 of Aadhaar e-KYC: verify the OTP entered by the client.
|
||||||
|
*
|
||||||
|
* @param refNumber Reference number returned by {@link #generateAadhaarOtp(String)}
|
||||||
|
* @param otp One-time password received by the client on their registered mobile
|
||||||
|
* @return verification result
|
||||||
|
*/
|
||||||
|
KycVerificationResult verifyAadhaar(String refNumber, String otp);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a PAN number against the Income Tax Department database via the configured 3rd-party API.
|
||||||
|
*
|
||||||
|
* @param panNumber 10-character PAN (e.g. {@code ABCDE1234F})
|
||||||
|
* @param clientName Name of the client – used for name-match verification
|
||||||
|
* @param dateOfBirth DOB in {@code yyyy-MM-dd} format (may be null for non-individual)
|
||||||
|
* @return verification result
|
||||||
|
*/
|
||||||
|
KycVerificationResult verifyPan(String panNumber, String clientName, String dateOfBirth);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a GSTIN number via the GST Common Portal API.
|
||||||
|
*
|
||||||
|
* @param gstinNumber 15-character GSTIN
|
||||||
|
* @return verification result
|
||||||
|
*/
|
||||||
|
KycVerificationResult verifyGstin(String gstinNumber);
|
||||||
|
}
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.kyc;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback (stub) implementation of {@link KycVerificationService}.
|
||||||
|
*
|
||||||
|
* <p><strong>NOT active in production.</strong> The primary implementation is
|
||||||
|
* {@link LincPayKycVerificationServiceImpl}, which is marked {@code @Primary}.
|
||||||
|
* This stub is retained as a compile-time safety net and for local/unit-test use
|
||||||
|
* when LincPay credentials are unavailable.
|
||||||
|
*
|
||||||
|
* <p>To switch back to the stub (e.g. in tests), remove the {@code @Primary} annotation
|
||||||
|
* from {@link LincPayKycVerificationServiceImpl} and add {@code @Primary} here.
|
||||||
|
*/
|
||||||
|
// NOTE: intentionally NOT annotated with @Service to avoid ambiguous bean conflict.
|
||||||
|
// Activate it by adding @Service @Primary if the LincPay implementation is disabled.
|
||||||
|
public class KycVerificationServiceImpl implements KycVerificationService {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(KycVerificationServiceImpl.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KycVerificationResult generateAadhaarOtp(final String aadhaarNumber) {
|
||||||
|
LOG.info("[KYC-STUB] Aadhaar generate-OTP called. Replace with real LincPay implementation.");
|
||||||
|
// Stub: return a fake refNumber – real code calls LincPayKycVerificationServiceImpl
|
||||||
|
return KycVerificationResult.success("STUB-OTP-REF-" + System.currentTimeMillis(),
|
||||||
|
"{\"status\":\"SUCCESS\",\"message\":\"Stub: OTP sent\",\"refNumber\":\"STUB-REF\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KycVerificationResult verifyAadhaar(final String refNumber, final String otp) {
|
||||||
|
LOG.info("[KYC-STUB] Aadhaar verify-OTP called for refNumber: {}. Replace with LincPay.", refNumber);
|
||||||
|
return KycVerificationResult.success(refNumber,
|
||||||
|
"{\"status\":\"SUCCESS\",\"message\":\"Stub: Aadhaar OTP verified\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KycVerificationResult verifyPan(final String panNumber, final String clientName,
|
||||||
|
final String dateOfBirth) {
|
||||||
|
LOG.info("[KYC-STUB] PAN verification called for PAN: {}. Replace with LincPay.", panNumber);
|
||||||
|
return KycVerificationResult.success("STUB-PAN-REF-" + panNumber,
|
||||||
|
"{\"status\":\"SUCCESS\",\"message\":\"Stub: PAN verified\",\"pan\":\"" + panNumber + "\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KycVerificationResult verifyGstin(final String gstinNumber) {
|
||||||
|
LOG.info("[KYC-STUB] GSTIN verification called for GSTIN: {}. Replace with LincPay.", gstinNumber);
|
||||||
|
return KycVerificationResult.success("STUB-GSTIN-REF-" + gstinNumber,
|
||||||
|
"{\"status\":\"SUCCESS\",\"message\":\"Stub: GSTIN verified\",\"gstin\":\"" + gstinNumber + "\"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
+336
@@ -0,0 +1,336 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* 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.kyc;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.HttpClientErrorException;
|
||||||
|
import org.springframework.web.client.HttpServerErrorException;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Real implementation of {@link KycVerificationService} that calls the
|
||||||
|
* <a href="https://ekyc.lincpay.in/EKYC/">LincPay eKYC API</a>.
|
||||||
|
*
|
||||||
|
* <p>Configure the following properties in {@code application.properties}:
|
||||||
|
* <pre>
|
||||||
|
* fineract.kyc.lincpay.api-key=YOUR_API_KEY
|
||||||
|
* fineract.kyc.lincpay.secret=YOUR_SECRET
|
||||||
|
* fineract.kyc.lincpay.base-url=https://ekyc.lincpay.in/EKYC/ekyc
|
||||||
|
* fineract.kyc.lincpay.path.aadhaar-generate-otp=/aadhaar/generate-otp
|
||||||
|
* fineract.kyc.lincpay.path.aadhaar-verify-otp=/aadhaar/verify-otp
|
||||||
|
* fineract.kyc.lincpay.path.pan-validate=/validate-pan
|
||||||
|
* fineract.kyc.lincpay.path.gstin-validate=/validate-gstn
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <p><b>Aadhaar OTP flow (two-step):</b>
|
||||||
|
* <ol>
|
||||||
|
* <li>{@link #generateAadhaarOtp(String)} – takes full Aadhaar, triggers OTP to
|
||||||
|
* client's registered mobile, returns a {@code refNumber}.</li>
|
||||||
|
* <li>{@link #verifyAadhaar(String, String)} – takes the {@code refNumber} and the OTP
|
||||||
|
* entered by the client; returns success/failure.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p><b>PAN:</b> Single-step – {@link #verifyPan(String, String, String)} validates PAN,
|
||||||
|
* name, and optional DOB against the Income Tax Department records.
|
||||||
|
*
|
||||||
|
* <p><b>GSTIN:</b> Single-step – {@link #verifyGstin(String)} validates the GST
|
||||||
|
* registration number via the GST Common Portal.
|
||||||
|
*/
|
||||||
|
@Primary
|
||||||
|
@Service
|
||||||
|
public class LincPayKycVerificationServiceImpl implements KycVerificationService {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(LincPayKycVerificationServiceImpl.class);
|
||||||
|
|
||||||
|
@Value("${fineract.kyc.lincpay.api-key}")
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
|
@Value("${fineract.kyc.lincpay.secret}")
|
||||||
|
private String secret;
|
||||||
|
|
||||||
|
@Value("${fineract.kyc.lincpay.base-url}")
|
||||||
|
private String baseUrl;
|
||||||
|
|
||||||
|
@Value("${fineract.kyc.lincpay.path.aadhaar-generate-otp}")
|
||||||
|
private String aadhaarGenerateOtpPath;
|
||||||
|
|
||||||
|
@Value("${fineract.kyc.lincpay.path.aadhaar-verify-otp}")
|
||||||
|
private String aadhaarVerifyOtpPath;
|
||||||
|
|
||||||
|
@Value("${fineract.kyc.lincpay.path.pan-validate}")
|
||||||
|
private String panValidatePath;
|
||||||
|
|
||||||
|
@Value("${fineract.kyc.lincpay.path.gstin-validate}")
|
||||||
|
private String gstinValidatePath;
|
||||||
|
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
|
public LincPayKycVerificationServiceImpl() {
|
||||||
|
this.restTemplate = new RestTemplate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Aadhaar OTP Generate ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST https://ekyc.lincpay.in/EKYC/ekyc/aadhaar/generate-otp
|
||||||
|
* Body: { "aadhaarNumber": "...", "apikey": "...", "secrete": "..." }
|
||||||
|
* Returns a refNumber in the result's referenceId for use in the verify-otp call.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public KycVerificationResult generateAadhaarOtp(final String aadhaarNumber) {
|
||||||
|
final String url = baseUrl + aadhaarGenerateOtpPath;
|
||||||
|
try {
|
||||||
|
final JsonObject requestBody = new JsonObject();
|
||||||
|
requestBody.addProperty("aadhaarNumber", aadhaarNumber);
|
||||||
|
requestBody.addProperty("apikey", apiKey);
|
||||||
|
requestBody.addProperty("secrete", secret);
|
||||||
|
|
||||||
|
final String rawResponse = post(url, requestBody.toString());
|
||||||
|
final JsonObject response = parseJson(rawResponse);
|
||||||
|
|
||||||
|
if (isSuccess(response)) {
|
||||||
|
// LincPay nests reference_id inside "data" object
|
||||||
|
String refNumber = null;
|
||||||
|
if (response.has("data") && response.get("data").isJsonObject()) {
|
||||||
|
final JsonObject data = response.getAsJsonObject("data");
|
||||||
|
refNumber = extractField(data, "reference_id", "refNumber", "ref_number",
|
||||||
|
"transactionId", "txnId", "requestId");
|
||||||
|
}
|
||||||
|
// Fallback: check top-level fields
|
||||||
|
if (refNumber == null) {
|
||||||
|
refNumber = extractField(response, "reference_id", "refNumber", "ref_number",
|
||||||
|
"transaction_id", "transactionId", "txnId", "requestId");
|
||||||
|
}
|
||||||
|
if (refNumber == null) {
|
||||||
|
LOG.warn("LincPay Aadhaar generate-otp: success status but no refNumber in response: {}",
|
||||||
|
rawResponse);
|
||||||
|
return KycVerificationResult.failure("OTP sent but refNumber missing in response",
|
||||||
|
rawResponse);
|
||||||
|
}
|
||||||
|
LOG.info("LincPay Aadhaar OTP generated successfully. refNumber: {}", refNumber);
|
||||||
|
return KycVerificationResult.success(refNumber, rawResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String errorMsg = extractMessage(response);
|
||||||
|
LOG.warn("LincPay Aadhaar generate-otp failed: {}", errorMsg);
|
||||||
|
return KycVerificationResult.failure(errorMsg, rawResponse);
|
||||||
|
|
||||||
|
} catch (final HttpClientErrorException | HttpServerErrorException e) {
|
||||||
|
LOG.error("LincPay Aadhaar generate-otp HTTP error: {} {}", e.getStatusCode(), e.getResponseBodyAsString());
|
||||||
|
return KycVerificationResult.failure(
|
||||||
|
"OTP generation failed (HTTP " + e.getStatusCode() + "): " + e.getResponseBodyAsString(), null);
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.error("LincPay Aadhaar generate-otp unexpected error", e);
|
||||||
|
return KycVerificationResult.failure("OTP generation failed: " + e.getMessage(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Aadhaar OTP Verify ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST https://ekyc.lincpay.in/EKYC/ekyc/aadhaar/verify-otp
|
||||||
|
* Body: { "apikey": "...", "otp": "...", "refNumber": "...", "secrete": "..." }
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public KycVerificationResult verifyAadhaar(final String refNumber, final String otp) {
|
||||||
|
final String url = baseUrl + aadhaarVerifyOtpPath;
|
||||||
|
try {
|
||||||
|
final JsonObject requestBody = new JsonObject();
|
||||||
|
requestBody.addProperty("apikey", apiKey);
|
||||||
|
requestBody.addProperty("otp", otp);
|
||||||
|
requestBody.addProperty("refNumber", refNumber);
|
||||||
|
requestBody.addProperty("secrete", secret);
|
||||||
|
|
||||||
|
final String rawResponse = post(url, requestBody.toString());
|
||||||
|
final JsonObject response = parseJson(rawResponse);
|
||||||
|
|
||||||
|
if (isSuccess(response)) {
|
||||||
|
LOG.info("LincPay Aadhaar OTP verified successfully for refNumber: {}", refNumber);
|
||||||
|
return KycVerificationResult.success(refNumber, rawResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String errorMsg = extractMessage(response);
|
||||||
|
LOG.warn("LincPay Aadhaar verify-otp failed for refNumber {}: {}", refNumber, errorMsg);
|
||||||
|
return KycVerificationResult.failure(errorMsg, rawResponse);
|
||||||
|
|
||||||
|
} catch (final HttpClientErrorException | HttpServerErrorException e) {
|
||||||
|
LOG.error("LincPay Aadhaar verify-otp HTTP error: {} {}", e.getStatusCode(), e.getResponseBodyAsString());
|
||||||
|
return KycVerificationResult.failure(
|
||||||
|
"Aadhaar OTP verification failed (HTTP " + e.getStatusCode() + "): "
|
||||||
|
+ e.getResponseBodyAsString(), null);
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.error("LincPay Aadhaar verify-otp unexpected error", e);
|
||||||
|
return KycVerificationResult.failure("Aadhaar OTP verification failed: " + e.getMessage(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PAN Validation ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST https://ekyc.lincpay.in/EKYC/ekyc/validate-pan
|
||||||
|
* Body: { "apikey": "...", "dob": "...", "name": "...", "panNumber": "...", "secrete": "..." }
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public KycVerificationResult verifyPan(final String panNumber, final String clientName,
|
||||||
|
final String dateOfBirth) {
|
||||||
|
final String url = baseUrl + panValidatePath;
|
||||||
|
try {
|
||||||
|
final JsonObject requestBody = new JsonObject();
|
||||||
|
requestBody.addProperty("apikey", apiKey);
|
||||||
|
requestBody.addProperty("panNumber", panNumber);
|
||||||
|
requestBody.addProperty("name", clientName != null ? clientName : "");
|
||||||
|
requestBody.addProperty("dob", dateOfBirth != null ? dateOfBirth : "");
|
||||||
|
requestBody.addProperty("secrete", secret);
|
||||||
|
|
||||||
|
LOG.info("LincPay PAN request URL: {}, body: {}", url, requestBody);
|
||||||
|
final String rawResponse = post(url, requestBody.toString());
|
||||||
|
LOG.info("LincPay PAN raw response: {}", rawResponse);
|
||||||
|
final JsonObject response = parseJson(rawResponse);
|
||||||
|
|
||||||
|
if (isSuccess(response)) {
|
||||||
|
LOG.info("LincPay PAN verified successfully for PAN: {}", panNumber);
|
||||||
|
return KycVerificationResult.success("PAN-" + panNumber, rawResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String errorMsg = extractMessage(response);
|
||||||
|
LOG.warn("LincPay PAN validation failed for PAN {}: {}", panNumber, errorMsg);
|
||||||
|
return KycVerificationResult.failure(errorMsg, rawResponse);
|
||||||
|
|
||||||
|
} catch (final HttpClientErrorException | HttpServerErrorException e) {
|
||||||
|
LOG.error("LincPay PAN validation HTTP error for PAN {}: {} {} body: {}", panNumber,
|
||||||
|
e.getStatusCode(), e.getResponseBodyAsString(), e.getResponseBodyAsString());
|
||||||
|
return KycVerificationResult.failure(
|
||||||
|
"PAN verification failed (HTTP " + e.getStatusCode() + "): "
|
||||||
|
+ e.getResponseBodyAsString(), null);
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.error("LincPay PAN validation unexpected error for PAN: {}", panNumber, e);
|
||||||
|
return KycVerificationResult.failure("PAN verification failed: " + e.getMessage(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GSTIN Validation ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST https://ekyc.lincpay.in/EKYC/ekyc/validate-gstn
|
||||||
|
* Body: { "apikey": "...", "gstNumber": "...", "secrete": "..." }
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public KycVerificationResult verifyGstin(final String gstinNumber) {
|
||||||
|
final String url = baseUrl + gstinValidatePath;
|
||||||
|
try {
|
||||||
|
final JsonObject requestBody = new JsonObject();
|
||||||
|
requestBody.addProperty("apikey", apiKey);
|
||||||
|
requestBody.addProperty("gstNumber", gstinNumber);
|
||||||
|
requestBody.addProperty("secrete", secret);
|
||||||
|
|
||||||
|
final String rawResponse = post(url, requestBody.toString());
|
||||||
|
final JsonObject response = parseJson(rawResponse);
|
||||||
|
|
||||||
|
if (isSuccess(response)) {
|
||||||
|
LOG.info("LincPay GSTIN verified successfully for GSTIN: {}", gstinNumber);
|
||||||
|
return KycVerificationResult.success("GSTN-" + gstinNumber, rawResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String errorMsg = extractMessage(response);
|
||||||
|
LOG.warn("LincPay GSTIN validation failed for GSTIN {}: {}", gstinNumber, errorMsg);
|
||||||
|
return KycVerificationResult.failure(errorMsg, rawResponse);
|
||||||
|
|
||||||
|
} catch (final HttpClientErrorException | HttpServerErrorException e) {
|
||||||
|
LOG.error("LincPay GSTIN validation HTTP error for GSTIN {}: {} {}", gstinNumber,
|
||||||
|
e.getStatusCode(), e.getResponseBodyAsString());
|
||||||
|
return KycVerificationResult.failure(
|
||||||
|
"GSTIN verification failed (HTTP " + e.getStatusCode() + "): "
|
||||||
|
+ e.getResponseBodyAsString(), null);
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.error("LincPay GSTIN validation unexpected error for GSTIN: {}", gstinNumber, e);
|
||||||
|
return KycVerificationResult.failure("GSTIN verification failed: " + e.getMessage(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Internal helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private String post(final String url, final String requestBodyJson) {
|
||||||
|
final HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.set("accept", "*/*");
|
||||||
|
|
||||||
|
final HttpEntity<String> entity = new HttpEntity<>(requestBodyJson, headers);
|
||||||
|
final ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
|
||||||
|
return response.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject parseJson(final String raw) {
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
return new JsonObject();
|
||||||
|
}
|
||||||
|
final JsonElement parsed = JsonParser.parseString(raw);
|
||||||
|
return parsed.isJsonObject() ? parsed.getAsJsonObject() : new JsonObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LincPay may return status in several forms. Check common field names.
|
||||||
|
*/
|
||||||
|
private boolean isSuccess(final JsonObject response) {
|
||||||
|
for (final String key : new String[] { "status", "statusCode", "responseCode", "code" }) {
|
||||||
|
if (response.has(key) && !response.get(key).isJsonNull()) {
|
||||||
|
final String val = response.get(key).getAsString().trim();
|
||||||
|
if ("SUCCESS".equalsIgnoreCase(val) || "200".equals(val) || "1".equals(val)
|
||||||
|
|| "true".equalsIgnoreCase(val)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// explicit failure values
|
||||||
|
if ("FAILURE".equalsIgnoreCase(val) || "FAILED".equalsIgnoreCase(val)
|
||||||
|
|| "ERROR".equalsIgnoreCase(val)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (response.has("success") && !response.get("success").isJsonNull()) {
|
||||||
|
return response.get("success").getAsBoolean();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Try common field names to extract a string value from the response. */
|
||||||
|
private String extractField(final JsonObject response, final String... fieldNames) {
|
||||||
|
for (final String field : fieldNames) {
|
||||||
|
if (response.has(field) && !response.get(field).isJsonNull()) {
|
||||||
|
return response.get(field).getAsString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractMessage(final JsonObject response) {
|
||||||
|
final String msg = extractField(response, "message", "msg", "error", "description",
|
||||||
|
"errorMessage", "errorDesc", "remarks");
|
||||||
|
return msg != null ? msg : "Verification failed – no error message returned by vendor";
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
-1
@@ -59,9 +59,13 @@ public final class ClientFamilyMemberCommandFromApiJsonDeserializer {
|
|||||||
public static final String LOCALE = "locale";
|
public static final String LOCALE = "locale";
|
||||||
public static final String DATE_FORMAT = "dateFormat";
|
public static final String DATE_FORMAT = "dateFormat";
|
||||||
public static final String FAMILY_MEMBERS = "familyMembers";
|
public static final String FAMILY_MEMBERS = "familyMembers";
|
||||||
|
public static final String NOMINATION_TYPE = "nominationType";
|
||||||
|
public static final String PERCENTAGE_SHARE = "percentageShare";
|
||||||
|
public static final String REMARKS = "remarks";
|
||||||
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(
|
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(
|
||||||
Arrays.asList(ID, CLIENT_ID, FIRST_NAME, MIDDLE_NAME, LAST_NAME, QUALIFICATION, MOBILE_NUMBER, AGE, IS_DEPENDENT,
|
Arrays.asList(ID, CLIENT_ID, FIRST_NAME, MIDDLE_NAME, LAST_NAME, QUALIFICATION, MOBILE_NUMBER, AGE, IS_DEPENDENT,
|
||||||
RELATIONSHIP_ID, MARITAL_STATUS_ID, GENDER_ID, DATE_OF_BIRTH, PROFESSION_ID, LOCALE, DATE_FORMAT, FAMILY_MEMBERS));
|
RELATIONSHIP_ID, MARITAL_STATUS_ID, GENDER_ID, DATE_OF_BIRTH, PROFESSION_ID, LOCALE, DATE_FORMAT, FAMILY_MEMBERS,
|
||||||
|
NOMINATION_TYPE, PERCENTAGE_SHARE, REMARKS));
|
||||||
public static final String FAMILY_MEMBERS1 = "FamilyMembers";
|
public static final String FAMILY_MEMBERS1 = "FamilyMembers";
|
||||||
public static final String RELATION_SHIP_ID = "relationShipId";
|
public static final String RELATION_SHIP_ID = "relationShipId";
|
||||||
private final FromJsonHelper fromApiJsonHelper;
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
@@ -188,6 +192,21 @@ public final class ClientFamilyMemberCommandFromApiJsonDeserializer {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.extractStringNamed(NOMINATION_TYPE, element) != null) {
|
||||||
|
final String nominationType = this.fromApiJsonHelper.extractStringNamed(NOMINATION_TYPE, element);
|
||||||
|
baseDataValidator.reset().parameter(NOMINATION_TYPE).value(nominationType).notBlank().notExceedingLengthOf(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(PERCENTAGE_SHARE, element) != null) {
|
||||||
|
final java.math.BigDecimal percentageShare = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(PERCENTAGE_SHARE, element);
|
||||||
|
baseDataValidator.reset().parameter(PERCENTAGE_SHARE).value(percentageShare).notNull().zeroOrPositiveAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.extractStringNamed(REMARKS, element) != null) {
|
||||||
|
final String remarks = this.fromApiJsonHelper.extractStringNamed(REMARKS, element);
|
||||||
|
baseDataValidator.reset().parameter(REMARKS).value(remarks).notExceedingLengthOf(500);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateForUpdate(final long familyMemberId, String json) {
|
public void validateForUpdate(final long familyMemberId, String json) {
|
||||||
@@ -258,6 +277,21 @@ public final class ClientFamilyMemberCommandFromApiJsonDeserializer {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.extractStringNamed(NOMINATION_TYPE, element) != null) {
|
||||||
|
final String nominationType = this.fromApiJsonHelper.extractStringNamed(NOMINATION_TYPE, element);
|
||||||
|
baseDataValidator.reset().parameter(NOMINATION_TYPE).value(nominationType).notBlank().notExceedingLengthOf(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(PERCENTAGE_SHARE, element) != null) {
|
||||||
|
final java.math.BigDecimal percentageShare = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(PERCENTAGE_SHARE, element);
|
||||||
|
baseDataValidator.reset().parameter(PERCENTAGE_SHARE).value(percentageShare).notNull().zeroOrPositiveAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.extractStringNamed(REMARKS, element) != null) {
|
||||||
|
final String remarks = this.fromApiJsonHelper.extractStringNamed(REMARKS, element);
|
||||||
|
baseDataValidator.reset().parameter(REMARKS).value(remarks).notExceedingLengthOf(500);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateForDelete(final long familyMemberId) {
|
public void validateForDelete(final long familyMemberId) {
|
||||||
|
|||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.serialization;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
|
import org.apache.fineract.portfolio.client.command.ClientKycCommand;
|
||||||
|
import org.apache.fineract.portfolio.client.data.ClientKycApiConstants;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ClientKycCommandFromApiJsonDeserializer {
|
||||||
|
|
||||||
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
|
||||||
|
public ClientKycCommand commandFromApiJson(final String json, final Set<String> supportedParams) {
|
||||||
|
if (StringUtils.isBlank(json)) {
|
||||||
|
throw new InvalidJsonException();
|
||||||
|
}
|
||||||
|
|
||||||
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||||
|
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParams);
|
||||||
|
|
||||||
|
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||||
|
|
||||||
|
final Integer kycType = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(
|
||||||
|
ClientKycApiConstants.kycTypeParam, element);
|
||||||
|
final Integer riskCategory = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(
|
||||||
|
ClientKycApiConstants.riskCategoryParam, element);
|
||||||
|
final String panNumber = this.fromApiJsonHelper.extractStringNamed(
|
||||||
|
ClientKycApiConstants.panNumberParam, element);
|
||||||
|
final String aadhaarNumberMasked = this.fromApiJsonHelper.extractStringNamed(
|
||||||
|
ClientKycApiConstants.aadhaarNumberMaskedParam, element);
|
||||||
|
final String gstinNumber = this.fromApiJsonHelper.extractStringNamed(
|
||||||
|
ClientKycApiConstants.gstinNumberParam, element);
|
||||||
|
final String remarks = this.fromApiJsonHelper.extractStringNamed(
|
||||||
|
ClientKycApiConstants.remarksParam, element);
|
||||||
|
final String ckycNumber = this.fromApiJsonHelper.extractStringNamed(
|
||||||
|
ClientKycApiConstants.ckycNumberParam, element);
|
||||||
|
final Boolean verifyPan = this.fromApiJsonHelper.extractBooleanNamed(
|
||||||
|
ClientKycApiConstants.verifyPanParam, element);
|
||||||
|
final Boolean verifyAadhaar = this.fromApiJsonHelper.extractBooleanNamed(
|
||||||
|
ClientKycApiConstants.verifyAadhaarParam, element);
|
||||||
|
final Boolean verifyGstin = this.fromApiJsonHelper.extractBooleanNamed(
|
||||||
|
ClientKycApiConstants.verifyGstinParam, element);
|
||||||
|
|
||||||
|
// Aadhaar OTP flow – full Aadhaar (generate-otp) and refNumber+OTP (verify-otp)
|
||||||
|
final String aadhaarNumber = this.fromApiJsonHelper.extractStringNamed(
|
||||||
|
ClientKycApiConstants.aadhaarNumberParam, element);
|
||||||
|
final String aadhaarRefNumber = this.fromApiJsonHelper.extractStringNamed(
|
||||||
|
ClientKycApiConstants.aadhaarRefNumberParam, element);
|
||||||
|
final String aadhaarOtp = this.fromApiJsonHelper.extractStringNamed(
|
||||||
|
ClientKycApiConstants.aadhaarOtpParam, element);
|
||||||
|
|
||||||
|
return new ClientKycCommand(kycType, riskCategory, panNumber, aadhaarNumberMasked,
|
||||||
|
gstinNumber, remarks, ckycNumber, verifyPan, verifyAadhaar, verifyGstin,
|
||||||
|
aadhaarNumber, aadhaarRefNumber, aadhaarOtp);
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-2
@@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.client.service;
|
package org.apache.fineract.portfolio.client.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
@@ -55,7 +56,8 @@ public class ClientFamilyMembersReadPlatformServiceImpl implements ClientFamilyM
|
|||||||
return "fmb.id AS id, fmb.client_id AS clientId, fmb.firstname AS firstName, fmb.middlename AS middleName,"
|
return "fmb.id AS id, fmb.client_id AS clientId, fmb.firstname AS firstName, fmb.middlename AS middleName,"
|
||||||
+ "fmb.lastname AS lastName,fmb.qualification AS qualification,fmb.mobile_number as mobileNumber,fmb.age as age,fmb.is_dependent as isDependent,cv.code_value AS relationship,fmb.relationship_cv_id AS relationshipId,"
|
+ "fmb.lastname AS lastName,fmb.qualification AS qualification,fmb.mobile_number as mobileNumber,fmb.age as age,fmb.is_dependent as isDependent,cv.code_value AS relationship,fmb.relationship_cv_id AS relationshipId,"
|
||||||
+ "c.code_value AS maritalStatus,fmb.marital_status_cv_id AS maritalStatusId,"
|
+ "c.code_value AS maritalStatus,fmb.marital_status_cv_id AS maritalStatusId,"
|
||||||
+ "c1.code_value AS gender, fmb.gender_cv_id AS genderId, fmb.date_of_birth AS dateOfBirth, c2.code_value AS profession, fmb.profession_cv_id AS professionId"
|
+ "c1.code_value AS gender, fmb.gender_cv_id AS genderId, fmb.date_of_birth AS dateOfBirth, c2.code_value AS profession, fmb.profession_cv_id AS professionId,"
|
||||||
|
+ "fmb.nomination_type AS nominationType, fmb.percentage_share AS percentageShare, fmb.remarks AS remarks"
|
||||||
+ " FROM m_family_members fmb" + " LEFT JOIN m_code_value cv ON fmb.relationship_cv_id=cv.id"
|
+ " FROM m_family_members fmb" + " LEFT JOIN m_code_value cv ON fmb.relationship_cv_id=cv.id"
|
||||||
+ " LEFT JOIN m_code_value c ON fmb.marital_status_cv_id=c.id" + " LEFT JOIN m_code_value c1 ON fmb.gender_cv_id=c1.id"
|
+ " LEFT JOIN m_code_value c ON fmb.marital_status_cv_id=c.id" + " LEFT JOIN m_code_value c1 ON fmb.gender_cv_id=c1.id"
|
||||||
+ " LEFT JOIN m_code_value c2 ON fmb.profession_cv_id=c2.id";
|
+ " LEFT JOIN m_code_value c2 ON fmb.profession_cv_id=c2.id";
|
||||||
@@ -81,11 +83,15 @@ public class ClientFamilyMembersReadPlatformServiceImpl implements ClientFamilyM
|
|||||||
final LocalDate dateOfBirth = JdbcSupport.getLocalDate(rs, "dateOfBirth");
|
final LocalDate dateOfBirth = JdbcSupport.getLocalDate(rs, "dateOfBirth");
|
||||||
final String profession = rs.getString("profession");
|
final String profession = rs.getString("profession");
|
||||||
final long professionId = rs.getLong("professionId");
|
final long professionId = rs.getLong("professionId");
|
||||||
|
final String nominationType = rs.getString("nominationType");
|
||||||
|
final BigDecimal percentageShare = rs.getBigDecimal("percentageShare");
|
||||||
|
final String remarks = rs.getString("remarks");
|
||||||
|
|
||||||
return ClientFamilyMembersData.builder().id(id).clientId(clientId).firstName(firstName).middleName(middleName)
|
return ClientFamilyMembersData.builder().id(id).clientId(clientId).firstName(firstName).middleName(middleName)
|
||||||
.lastName(lastName).qualification(qualification).mobileNumber(mobileNumber).age(age).isDependent(isDependent)
|
.lastName(lastName).qualification(qualification).mobileNumber(mobileNumber).age(age).isDependent(isDependent)
|
||||||
.relationship(relationship).relationshipId(relationshipId).maritalStatus(maritalStatus).maritalStatusId(maritalStatusId)
|
.relationship(relationship).relationshipId(relationshipId).maritalStatus(maritalStatus).maritalStatusId(maritalStatusId)
|
||||||
.gender(gender).genderId(genderId).dateOfBirth(dateOfBirth).profession(profession).professionId(professionId).build();
|
.gender(gender).genderId(genderId).dateOfBirth(dateOfBirth).profession(profession).professionId(professionId)
|
||||||
|
.nominationType(nominationType).percentageShare(percentageShare).remarks(remarks).build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+38
-2
@@ -22,6 +22,7 @@ package org.apache.fineract.portfolio.client.service;
|
|||||||
import com.google.gson.JsonArray;
|
import com.google.gson.JsonArray;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.time.format.DateTimeFormatterBuilder;
|
import java.time.format.DateTimeFormatterBuilder;
|
||||||
@@ -83,6 +84,9 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
|
|||||||
Long age = null;
|
Long age = null;
|
||||||
Boolean isDependent = false;
|
Boolean isDependent = false;
|
||||||
LocalDate dateOfBirth = null;
|
LocalDate dateOfBirth = null;
|
||||||
|
String nominationType = null;
|
||||||
|
BigDecimal percentageShare = null;
|
||||||
|
String remarks = null;
|
||||||
|
|
||||||
this.context.authenticatedUser();
|
this.context.authenticatedUser();
|
||||||
apiJsonDeserializer.validateForCreate(clientId, command.json());
|
apiJsonDeserializer.validateForCreate(clientId, command.json());
|
||||||
@@ -95,6 +99,9 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
|
|||||||
mobileNumber = command.stringValueOfParameterNamed("mobileNumber");
|
mobileNumber = command.stringValueOfParameterNamed("mobileNumber");
|
||||||
age = command.longValueOfParameterNamed("age");
|
age = command.longValueOfParameterNamed("age");
|
||||||
isDependent = command.booleanObjectValueOfParameterNamed("isDependent");
|
isDependent = command.booleanObjectValueOfParameterNamed("isDependent");
|
||||||
|
nominationType = command.stringValueOfParameterNamed("nominationType");
|
||||||
|
percentageShare = command.bigDecimalValueOfParameterNamed("percentageShare");
|
||||||
|
remarks = command.stringValueOfParameterNamed("remarks");
|
||||||
|
|
||||||
if (command.longValueOfParameterNamed("relationshipId") != null) {
|
if (command.longValueOfParameterNamed("relationshipId") != null) {
|
||||||
relationshipId = command.longValueOfParameterNamed("relationshipId");
|
relationshipId = command.longValueOfParameterNamed("relationshipId");
|
||||||
@@ -119,7 +126,8 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
|
|||||||
dateOfBirth = command.localDateValueOfParameterNamed("dateOfBirth");
|
dateOfBirth = command.localDateValueOfParameterNamed("dateOfBirth");
|
||||||
|
|
||||||
ClientFamilyMembers clientFamilyMembers = ClientFamilyMembers.fromJson(client, firstName, middleName, lastName, qualification,
|
ClientFamilyMembers clientFamilyMembers = ClientFamilyMembers.fromJson(client, firstName, middleName, lastName, qualification,
|
||||||
mobileNumber, age, isDependent, relationship, maritalStatus, gender, dateOfBirth, profession);
|
mobileNumber, age, isDependent, relationship, maritalStatus, gender, dateOfBirth, profession,
|
||||||
|
nominationType, percentageShare, remarks);
|
||||||
|
|
||||||
this.clientFamilyRepository.saveAndFlush(clientFamilyMembers);
|
this.clientFamilyRepository.saveAndFlush(clientFamilyMembers);
|
||||||
|
|
||||||
@@ -146,6 +154,9 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
|
|||||||
String mobileNumber = "";
|
String mobileNumber = "";
|
||||||
Long age = null;
|
Long age = null;
|
||||||
Boolean isDependent = false;
|
Boolean isDependent = false;
|
||||||
|
String nominationType = null;
|
||||||
|
BigDecimal percentageShare = null;
|
||||||
|
String remarks = null;
|
||||||
|
|
||||||
this.context.authenticatedUser();
|
this.context.authenticatedUser();
|
||||||
|
|
||||||
@@ -231,8 +242,21 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (member.get("nominationType") != null) {
|
||||||
|
nominationType = member.get("nominationType").getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member.get("percentageShare") != null) {
|
||||||
|
percentageShare = member.get("percentageShare").getAsBigDecimal();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member.get("remarks") != null) {
|
||||||
|
remarks = member.get("remarks").getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
familyMember = ClientFamilyMembers.fromJson(client, firstName, middleName, lastName, qualification, mobileNumber, age,
|
familyMember = ClientFamilyMembers.fromJson(client, firstName, middleName, lastName, qualification, mobileNumber, age,
|
||||||
isDependent, relationship, maritalStatus, gender, dateOfBirth, profession);
|
isDependent, relationship, maritalStatus, gender, dateOfBirth, profession,
|
||||||
|
nominationType, percentageShare, remarks);
|
||||||
|
|
||||||
this.clientFamilyRepository.saveAndFlush(familyMember);
|
this.clientFamilyRepository.saveAndFlush(familyMember);
|
||||||
|
|
||||||
@@ -342,6 +366,18 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command.stringValueOfParameterNamed("nominationType") != null) {
|
||||||
|
clientFamilyMember.setNominationType(command.stringValueOfParameterNamed("nominationType"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.bigDecimalValueOfParameterNamed("percentageShare") != null) {
|
||||||
|
clientFamilyMember.setPercentageShare(command.bigDecimalValueOfParameterNamed("percentageShare"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.stringValueOfParameterNamed("remarks") != null) {
|
||||||
|
clientFamilyMember.setRemarks(command.stringValueOfParameterNamed("remarks"));
|
||||||
|
}
|
||||||
|
|
||||||
// ClientFamilyMembers
|
// ClientFamilyMembers
|
||||||
// clientFamilyMembers=ClientFamilyMembers.fromJson(client, firstName,
|
// clientFamilyMembers=ClientFamilyMembers.fromJson(client, firstName,
|
||||||
// middleName, lastName, qualification, relationship, maritalStatus,
|
// middleName, lastName, qualification, relationship, maritalStatus,
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.service;
|
||||||
|
|
||||||
|
import org.apache.fineract.portfolio.client.data.ClientKycData;
|
||||||
|
|
||||||
|
public interface ClientKycReadPlatformService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the KYC record for a specific KYC id.
|
||||||
|
*/
|
||||||
|
ClientKycData retrieveClientKyc(Long kycId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the KYC record for a client.
|
||||||
|
*/
|
||||||
|
ClientKycData retrieveClientKycByClientId(Long clientId);
|
||||||
|
}
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* 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.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.portfolio.client.data.ClientKycData;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientKyc;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientKycRepositoryWrapper;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientKycStatus;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientKycType;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.KycRiskCategory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class ClientKycReadPlatformServiceImpl implements ClientKycReadPlatformService {
|
||||||
|
|
||||||
|
private final ClientKycRepositoryWrapper kycRepositoryWrapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientKycData retrieveClientKyc(final Long kycId) {
|
||||||
|
final ClientKyc kyc = kycRepositoryWrapper.findOneWithNotFoundDetection(kycId);
|
||||||
|
return toData(kyc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientKycData retrieveClientKycByClientId(final Long clientId) {
|
||||||
|
final ClientKyc kyc = kycRepositoryWrapper.findByClientIdWithNotFoundDetection(clientId);
|
||||||
|
return toData(kyc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientKycData toData(final ClientKyc kyc) {
|
||||||
|
final ClientKycStatus status = ClientKycStatus.fromInt(kyc.getKycStatusValue());
|
||||||
|
final ClientKycType type = ClientKycType.fromInt(kyc.getKycTypeValue());
|
||||||
|
final KycRiskCategory risk = KycRiskCategory.fromInt(kyc.getRiskCategoryValue());
|
||||||
|
|
||||||
|
return ClientKycData.builder()
|
||||||
|
.id(kyc.getId())
|
||||||
|
.clientId(kyc.getClient().getId())
|
||||||
|
.clientDisplayName(kyc.getClient().getDisplayName())
|
||||||
|
.kycStatus(status.getValue())
|
||||||
|
.kycStatusCode(status.getCode())
|
||||||
|
.kycType(type.getValue())
|
||||||
|
.kycTypeCode(type.getCode())
|
||||||
|
.riskCategory(risk.getValue())
|
||||||
|
.riskCategoryCode(risk.getCode())
|
||||||
|
.panNumber(kyc.getPanNumber())
|
||||||
|
.panVerified(kyc.isPanVerified())
|
||||||
|
.panVerifiedOn(kyc.getPanVerifiedOn())
|
||||||
|
.panFullName(kyc.getPanFullName())
|
||||||
|
.panAadhaarSeedingStatus(kyc.getPanAadhaarSeedingStatus())
|
||||||
|
.panCategory(kyc.getPanCategory())
|
||||||
|
.panStatus(kyc.getPanStatus())
|
||||||
|
.aadhaarNumberMasked(kyc.getAadhaarNumberMasked())
|
||||||
|
.aadhaarVerified(kyc.isAadhaarVerified())
|
||||||
|
.aadhaarVerifiedOn(kyc.getAadhaarVerifiedOn())
|
||||||
|
.gstinNumber(kyc.getGstinNumber())
|
||||||
|
.gstinVerified(kyc.isGstinVerified())
|
||||||
|
.gstinVerifiedOn(kyc.getGstinVerifiedOn())
|
||||||
|
.ckycNumber(kyc.getCkycNumber())
|
||||||
|
.initiatedOn(kyc.getInitiatedOn())
|
||||||
|
.verifiedOn(kyc.getVerifiedOn())
|
||||||
|
.rejectedOn(kyc.getRejectedOn())
|
||||||
|
.expiryDate(kyc.getExpiryDate())
|
||||||
|
.remarks(kyc.getRemarks())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.service;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
|
|
||||||
|
public interface ClientKycWritePlatformService {
|
||||||
|
|
||||||
|
/** Initiate a new KYC record for the given client (status: PENDING). */
|
||||||
|
CommandProcessingResult initiateKyc(Long clientId, JsonCommand command);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates a KYC record inline during client registration.
|
||||||
|
* The KYC JSON is the nested {@code kyc} object from the client creation payload.
|
||||||
|
* Runs within the same transaction as the parent client creation.
|
||||||
|
*/
|
||||||
|
void initiateKycInline(Client client, String kycJson);
|
||||||
|
|
||||||
|
/** Update PAN / Aadhaar / GSTIN details; triggers IN_PROGRESS status. */
|
||||||
|
CommandProcessingResult updateKyc(Long clientId, Long kycId, JsonCommand command);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 1 of Aadhaar e-KYC: calls LincPay generate-otp with the client's full Aadhaar.
|
||||||
|
* The full Aadhaar is NOT stored. The returned {@code CommandProcessingResult} includes
|
||||||
|
* a {@code "aadhaarRefNumber"} changes entry that must be passed back to the client-facing
|
||||||
|
* app so they can present it in the subsequent verify-otp step.
|
||||||
|
*/
|
||||||
|
CommandProcessingResult generateAadhaarOtp(Long clientId, Long kycId, JsonCommand command);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger 3rd-party verification for selected document types and, if all pass,
|
||||||
|
* mark KYC as VERIFIED.
|
||||||
|
*/
|
||||||
|
CommandProcessingResult verifyKyc(Long clientId, Long kycId, JsonCommand command);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify PAN via LincPay and mark panVerified on the KYC record.
|
||||||
|
* The KYC is NOT auto-rejected on failure – returns an error response so the UI can retry.
|
||||||
|
*/
|
||||||
|
CommandProcessingResult verifyPan(Long clientId, Long kycId, JsonCommand command);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 2 of Aadhaar e-KYC: verify the OTP using refNumber returned by generate-otp.
|
||||||
|
* Marks aadhaarVerified on the KYC record on success.
|
||||||
|
*/
|
||||||
|
CommandProcessingResult verifyAadhaar(Long clientId, Long kycId, JsonCommand command);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify GSTIN via LincPay and mark gstinVerified on the KYC record.
|
||||||
|
* The KYC is NOT auto-rejected on failure – returns an error response so the UI can retry.
|
||||||
|
*/
|
||||||
|
CommandProcessingResult verifyGstin(Long clientId, Long kycId, JsonCommand command);
|
||||||
|
|
||||||
|
/** Reject the KYC; stores a rejection reason. */
|
||||||
|
CommandProcessingResult rejectKyc(Long clientId, Long kycId, JsonCommand command);
|
||||||
|
}
|
||||||
+460
@@ -0,0 +1,460 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* 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.service;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import jakarta.persistence.PersistenceException;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.client.command.ClientKycCommand;
|
||||||
|
import org.apache.fineract.portfolio.client.data.ClientKycApiConstants;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientKyc;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientKycRepositoryWrapper;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientKycStatus;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||||
|
import org.apache.fineract.portfolio.client.exception.ClientKycAlreadyExistsException;
|
||||||
|
import org.apache.fineract.portfolio.client.exception.ClientKycInvalidStateTransitionException;
|
||||||
|
import org.apache.fineract.portfolio.client.kyc.KycVerificationResult;
|
||||||
|
import org.apache.fineract.portfolio.client.kyc.KycVerificationService;
|
||||||
|
import org.apache.fineract.portfolio.client.serialization.ClientKycCommandFromApiJsonDeserializer;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
|
import org.springframework.orm.jpa.JpaSystemException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKycWritePlatformService {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ClientKycWritePlatformServiceJpaRepositoryImpl.class);
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final ClientRepositoryWrapper clientRepository;
|
||||||
|
private final ClientKycRepositoryWrapper kycRepositoryWrapper;
|
||||||
|
private final ClientKycCommandFromApiJsonDeserializer deserializer;
|
||||||
|
private final KycVerificationService kycVerificationService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult initiateKyc(final Long clientId, final JsonCommand command) {
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
|
||||||
|
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
||||||
|
command.json(), ClientKycApiConstants.INITIATE_REQUEST_PARAMS);
|
||||||
|
kycCommand.validateForInitiate();
|
||||||
|
|
||||||
|
if (kycRepositoryWrapper.existsByClientId(clientId)) {
|
||||||
|
throw new ClientKycAlreadyExistsException(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
|
||||||
|
final ClientKyc kyc = ClientKyc.initiateKyc(client, command);
|
||||||
|
kycRepositoryWrapper.saveAndFlush(kyc);
|
||||||
|
|
||||||
|
return new CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(command.commandId())
|
||||||
|
.withOfficeId(client.officeId())
|
||||||
|
.withClientId(clientId)
|
||||||
|
.withEntityId(kyc.getId())
|
||||||
|
.build();
|
||||||
|
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
|
||||||
|
handleDataIntegrityIssues(dve.getMostSpecificCause(), dve);
|
||||||
|
return CommandProcessingResult.empty();
|
||||||
|
} catch (final PersistenceException dve) {
|
||||||
|
handleDataIntegrityIssues(ExceptionUtils.getRootCause(dve.getCause()), dve);
|
||||||
|
return CommandProcessingResult.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult updateKyc(final Long clientId, final Long kycId, final JsonCommand command) {
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
|
||||||
|
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
||||||
|
command.json(), ClientKycApiConstants.UPDATE_REQUEST_PARAMS);
|
||||||
|
kycCommand.validateForUpdate();
|
||||||
|
|
||||||
|
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
|
||||||
|
final ClientKyc kyc = kycRepositoryWrapper.findOneWithNotFoundDetection(kycId);
|
||||||
|
|
||||||
|
final ClientKycStatus currentStatus = kyc.getKycStatus();
|
||||||
|
if (currentStatus.isVerified()) {
|
||||||
|
throw new ClientKycInvalidStateTransitionException("update", currentStatus.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, Object> changes = kyc.update(command);
|
||||||
|
|
||||||
|
if (!changes.isEmpty()) {
|
||||||
|
// Move to IN_PROGRESS when documents are updated
|
||||||
|
if (!currentStatus.isInProgress()) {
|
||||||
|
kyc.markInProgress();
|
||||||
|
changes.put("kycStatus", ClientKycStatus.IN_PROGRESS.getValue());
|
||||||
|
}
|
||||||
|
kycRepositoryWrapper.saveAndFlush(kyc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(command.commandId())
|
||||||
|
.withOfficeId(client.officeId())
|
||||||
|
.withClientId(clientId)
|
||||||
|
.withEntityId(kycId)
|
||||||
|
.with(changes)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult generateAadhaarOtp(final Long clientId, final Long kycId,
|
||||||
|
final JsonCommand command) {
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
|
||||||
|
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
||||||
|
command.json(), ClientKycApiConstants.AADHAAR_GENERATE_OTP_REQUEST_PARAMS);
|
||||||
|
kycCommand.validateForAadhaarGenerateOtp();
|
||||||
|
|
||||||
|
// Full Aadhaar is used only for the API call – never stored
|
||||||
|
final KycVerificationResult result = kycVerificationService.generateAadhaarOtp(
|
||||||
|
kycCommand.getAadhaarNumber());
|
||||||
|
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
throw new PlatformServiceUnavailableException(
|
||||||
|
"error.msg.client.kyc.aadhaar.otp.generation.failed",
|
||||||
|
"Aadhaar OTP generation failed: " + result.getErrorMessage(),
|
||||||
|
result.getErrorMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
|
||||||
|
return new CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(command.commandId())
|
||||||
|
.withOfficeId(client.officeId())
|
||||||
|
.withClientId(clientId)
|
||||||
|
.withEntityId(kycId)
|
||||||
|
.with(Map.of("aadhaarRefNumber", result.getReferenceId()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult verifyKyc(final Long clientId, final Long kycId, final JsonCommand command) {
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
|
||||||
|
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
||||||
|
command.json(), ClientKycApiConstants.VERIFY_REQUEST_PARAMS);
|
||||||
|
kycCommand.validateForVerify();
|
||||||
|
|
||||||
|
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
|
||||||
|
final ClientKyc kyc = kycRepositoryWrapper.findOneWithNotFoundDetection(kycId);
|
||||||
|
|
||||||
|
final ClientKycStatus currentStatus = kyc.getKycStatus();
|
||||||
|
if (currentStatus.isRejected()) {
|
||||||
|
throw new ClientKycInvalidStateTransitionException("verify", currentStatus.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 3rd-party verifications ───────────────────────────────────────────
|
||||||
|
|
||||||
|
if (Boolean.TRUE.equals(kycCommand.getVerifyPan()) && kyc.getPanNumber() != null) {
|
||||||
|
final String panDob = client.dateOfBirth() != null
|
||||||
|
? client.dateOfBirth().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")) : null;
|
||||||
|
final KycVerificationResult result = kycVerificationService.verifyPan(
|
||||||
|
kyc.getPanNumber(), client.getDisplayName(), panDob);
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
final JsonObject panData = extractPanData(result.getRawResponse());
|
||||||
|
kyc.markPanVerified(
|
||||||
|
getJsonString(panData, "full_name"),
|
||||||
|
getJsonString(panData, "aadhaar_seeding_status"),
|
||||||
|
getJsonString(panData, "category"),
|
||||||
|
getJsonString(panData, "status"));
|
||||||
|
} else {
|
||||||
|
LOG.warn("PAN verification failed for client {}: {}", clientId, result.getErrorMessage());
|
||||||
|
kyc.markRejected("PAN verification failed: " + result.getErrorMessage());
|
||||||
|
kycRepositoryWrapper.saveAndFlush(kyc);
|
||||||
|
return buildResult(command, client, clientId, kycId, Map.of("kycStatus", kyc.getKycStatusValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Boolean.TRUE.equals(kycCommand.getVerifyAadhaar())) {
|
||||||
|
// refNumber and otp are validated in kycCommand.validateForVerify() when verifyAadhaar=true
|
||||||
|
final KycVerificationResult result = kycVerificationService.verifyAadhaar(
|
||||||
|
kycCommand.getAadhaarRefNumber(), kycCommand.getAadhaarOtp());
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
kyc.markAadhaarVerified();
|
||||||
|
} else {
|
||||||
|
LOG.warn("Aadhaar OTP verification failed for client {}: {}", clientId, result.getErrorMessage());
|
||||||
|
kyc.markRejected("Aadhaar verification failed: " + result.getErrorMessage());
|
||||||
|
kycRepositoryWrapper.saveAndFlush(kyc);
|
||||||
|
return buildResult(command, client, clientId, kycId, Map.of("kycStatus", kyc.getKycStatusValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Boolean.TRUE.equals(kycCommand.getVerifyGstin()) && kyc.getGstinNumber() != null) {
|
||||||
|
final KycVerificationResult result = kycVerificationService.verifyGstin(kyc.getGstinNumber());
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
kyc.markGstinVerified();
|
||||||
|
} else {
|
||||||
|
LOG.warn("GSTIN verification failed for client {}: {}", clientId, result.getErrorMessage());
|
||||||
|
kyc.markRejected("GSTIN verification failed: " + result.getErrorMessage());
|
||||||
|
kycRepositoryWrapper.saveAndFlush(kyc);
|
||||||
|
return buildResult(command, client, clientId, kycId, Map.of("kycStatus", kyc.getKycStatusValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All requested verifications passed – mark VERIFIED
|
||||||
|
kyc.markVerified(kycCommand.getCkycNumber());
|
||||||
|
kycRepositoryWrapper.saveAndFlush(kyc);
|
||||||
|
|
||||||
|
return buildResult(command, client, clientId, kycId,
|
||||||
|
Map.of("kycStatus", kyc.getKycStatusValue(), "verifiedOn", kyc.getVerifiedOn()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult verifyPan(final Long clientId, final Long kycId, final JsonCommand command) {
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
LOG.info("verifyPan: START clientId={}, kycId={}", clientId, kycId);
|
||||||
|
|
||||||
|
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
||||||
|
command.json(), ClientKycApiConstants.VERIFY_PAN_REQUEST_PARAMS);
|
||||||
|
kycCommand.validateForVerifyPan();
|
||||||
|
|
||||||
|
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
|
||||||
|
final ClientKyc kyc = kycRepositoryWrapper.findOneWithNotFoundDetection(kycId);
|
||||||
|
|
||||||
|
LOG.info("verifyPan: client='{}', panNumber='{}', kycStatus={}, dateOfBirth={}",
|
||||||
|
client.getDisplayName(), kyc.getPanNumber(), kyc.getKycStatus(), client.dateOfBirth());
|
||||||
|
|
||||||
|
if (kyc.getKycStatus().isRejected()) {
|
||||||
|
throw new ClientKycInvalidStateTransitionException("verify-pan", kyc.getKycStatus().name());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kyc.getPanNumber() == null) {
|
||||||
|
throw new PlatformServiceUnavailableException(
|
||||||
|
"error.msg.client.kyc.pan.not.found",
|
||||||
|
"PAN number is not set on this KYC record. Update the KYC record first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final String dob = client.dateOfBirth() != null
|
||||||
|
? client.dateOfBirth().format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))
|
||||||
|
: null;
|
||||||
|
LOG.info("verifyPan: calling LincPay with panNumber='{}', name='{}', dob='{}'",
|
||||||
|
kyc.getPanNumber(), client.getDisplayName(), dob);
|
||||||
|
|
||||||
|
final KycVerificationResult result = kycVerificationService.verifyPan(
|
||||||
|
kyc.getPanNumber(), client.getDisplayName(), dob);
|
||||||
|
|
||||||
|
LOG.info("verifyPan: LincPay result success={}, referenceId={}, errorMessage={}, rawResponse={}",
|
||||||
|
result.isSuccess(), result.getReferenceId(), result.getErrorMessage(), result.getRawResponse());
|
||||||
|
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
LOG.error("verifyPan: FAILED for clientId={}, kycId={}, pan={}: {}",
|
||||||
|
clientId, kycId, kyc.getPanNumber(), result.getErrorMessage());
|
||||||
|
throw new PlatformServiceUnavailableException(
|
||||||
|
"error.msg.client.kyc.pan.verification.failed",
|
||||||
|
"PAN verification failed: " + result.getErrorMessage(),
|
||||||
|
result.getErrorMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
final JsonObject panData = extractPanData(result.getRawResponse());
|
||||||
|
kyc.markPanVerified(
|
||||||
|
getJsonString(panData, "full_name"),
|
||||||
|
getJsonString(panData, "aadhaar_seeding_status"),
|
||||||
|
getJsonString(panData, "category"),
|
||||||
|
getJsonString(panData, "status"));
|
||||||
|
kycRepositoryWrapper.saveAndFlush(kyc);
|
||||||
|
LOG.info("verifyPan: SUCCESS for clientId={}, kycId={}", clientId, kycId);
|
||||||
|
|
||||||
|
return buildResult(command, client, clientId, kycId,
|
||||||
|
Map.of("panVerified", true, "panVerifiedOn", kyc.getPanVerifiedOn()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult verifyAadhaar(final Long clientId, final Long kycId, final JsonCommand command) {
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
|
||||||
|
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
||||||
|
command.json(), ClientKycApiConstants.VERIFY_AADHAAR_REQUEST_PARAMS);
|
||||||
|
kycCommand.validateForVerifyAadhaar();
|
||||||
|
|
||||||
|
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
|
||||||
|
final ClientKyc kyc = kycRepositoryWrapper.findOneWithNotFoundDetection(kycId);
|
||||||
|
|
||||||
|
if (kyc.getKycStatus().isRejected()) {
|
||||||
|
throw new ClientKycInvalidStateTransitionException("verify-aadhaar", kyc.getKycStatus().name());
|
||||||
|
}
|
||||||
|
|
||||||
|
final KycVerificationResult result = kycVerificationService.verifyAadhaar(
|
||||||
|
kycCommand.getAadhaarRefNumber(), kycCommand.getAadhaarOtp());
|
||||||
|
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
throw new PlatformServiceUnavailableException(
|
||||||
|
"error.msg.client.kyc.aadhaar.verification.failed",
|
||||||
|
"Aadhaar OTP verification failed: " + result.getErrorMessage(),
|
||||||
|
result.getErrorMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
kyc.markAadhaarVerified();
|
||||||
|
kycRepositoryWrapper.saveAndFlush(kyc);
|
||||||
|
|
||||||
|
return buildResult(command, client, clientId, kycId,
|
||||||
|
Map.of("aadhaarVerified", true, "aadhaarVerifiedOn", kyc.getAadhaarVerifiedOn()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult verifyGstin(final Long clientId, final Long kycId, final JsonCommand command) {
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
|
||||||
|
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
||||||
|
command.json(), ClientKycApiConstants.VERIFY_GSTIN_REQUEST_PARAMS);
|
||||||
|
kycCommand.validateForVerifyGstin();
|
||||||
|
|
||||||
|
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
|
||||||
|
final ClientKyc kyc = kycRepositoryWrapper.findOneWithNotFoundDetection(kycId);
|
||||||
|
|
||||||
|
if (kyc.getKycStatus().isRejected()) {
|
||||||
|
throw new ClientKycInvalidStateTransitionException("verify-gstin", kyc.getKycStatus().name());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kyc.getGstinNumber() == null) {
|
||||||
|
throw new PlatformServiceUnavailableException(
|
||||||
|
"error.msg.client.kyc.gstin.not.found",
|
||||||
|
"GSTIN number is not set on this KYC record. Update the KYC record first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final KycVerificationResult result = kycVerificationService.verifyGstin(kyc.getGstinNumber());
|
||||||
|
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
throw new PlatformServiceUnavailableException(
|
||||||
|
"error.msg.client.kyc.gstin.verification.failed",
|
||||||
|
"GSTIN verification failed: " + result.getErrorMessage(),
|
||||||
|
result.getErrorMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
kyc.markGstinVerified();
|
||||||
|
kycRepositoryWrapper.saveAndFlush(kyc);
|
||||||
|
|
||||||
|
return buildResult(command, client, clientId, kycId,
|
||||||
|
Map.of("gstinVerified", true, "gstinVerifiedOn", kyc.getGstinVerifiedOn()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult rejectKyc(final Long clientId, final Long kycId, final JsonCommand command) {
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
|
||||||
|
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
||||||
|
command.json(), ClientKycApiConstants.REJECT_REQUEST_PARAMS);
|
||||||
|
kycCommand.validateForReject();
|
||||||
|
|
||||||
|
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
|
||||||
|
final ClientKyc kyc = kycRepositoryWrapper.findOneWithNotFoundDetection(kycId);
|
||||||
|
|
||||||
|
if (kyc.getKycStatus().isRejected()) {
|
||||||
|
throw new ClientKycInvalidStateTransitionException("reject", kyc.getKycStatus().name());
|
||||||
|
}
|
||||||
|
|
||||||
|
kyc.markRejected(kycCommand.getRemarks());
|
||||||
|
kycRepositoryWrapper.saveAndFlush(kyc);
|
||||||
|
|
||||||
|
return buildResult(command, client, clientId, kycId,
|
||||||
|
Map.of("kycStatus", kyc.getKycStatusValue(), "rejectedOn", kyc.getRejectedOn()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initiateKycInline(final Client client, final String kycJson) {
|
||||||
|
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
||||||
|
kycJson, ClientKycApiConstants.INITIATE_REQUEST_PARAMS);
|
||||||
|
kycCommand.validateForInitiate();
|
||||||
|
|
||||||
|
if (kycRepositoryWrapper.existsByClientId(client.getId())) {
|
||||||
|
throw new ClientKycAlreadyExistsException(client.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
final ClientKyc kyc = ClientKyc.initiateKyc(client,
|
||||||
|
kycCommand.getKycType(), kycCommand.getRiskCategory(),
|
||||||
|
kycCommand.getPanNumber(), kycCommand.getAadhaarNumberMasked(),
|
||||||
|
kycCommand.getGstinNumber(), kycCommand.getRemarks());
|
||||||
|
kycRepositoryWrapper.saveAndFlush(kyc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private CommandProcessingResult buildResult(final JsonCommand command, final Client client,
|
||||||
|
final Long clientId, final Long kycId, final Map<String, Object> changes) {
|
||||||
|
return new CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(command.commandId())
|
||||||
|
.withOfficeId(client.officeId())
|
||||||
|
.withClientId(clientId)
|
||||||
|
.withEntityId(kycId)
|
||||||
|
.with(changes)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the "data" object from a LincPay PAN API response.
|
||||||
|
* Expected shape: {"code":"200","data":{"full_name":"...","aadhaar_seeding_status":"y","category":"individual","status":"valid",...}}
|
||||||
|
*/
|
||||||
|
private JsonObject extractPanData(final String rawResponse) {
|
||||||
|
if (rawResponse == null || rawResponse.isBlank()) {
|
||||||
|
return new JsonObject();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final JsonElement root = JsonParser.parseString(rawResponse);
|
||||||
|
if (root.isJsonObject() && root.getAsJsonObject().has("data")
|
||||||
|
&& root.getAsJsonObject().get("data").isJsonObject()) {
|
||||||
|
return root.getAsJsonObject().getAsJsonObject("data");
|
||||||
|
}
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.warn("Failed to parse PAN response JSON: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return new JsonObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getJsonString(final JsonObject json, final String field) {
|
||||||
|
if (json.has(field) && !json.get(field).isJsonNull()) {
|
||||||
|
return json.get(field).getAsString();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleDataIntegrityIssues(final Throwable cause, final Exception dve) {
|
||||||
|
if (cause != null && cause.getMessage() != null
|
||||||
|
&& cause.getMessage().contains("uq_m_client_kyc_client_id")) {
|
||||||
|
throw new ClientKycAlreadyExistsException(-1L);
|
||||||
|
}
|
||||||
|
LOG.error("Unexpected data integrity issue in KYC.", dve);
|
||||||
|
throw ErrorHandler.getMappable(dve, "error.msg.client.kyc.unknown.data.integrity.issue",
|
||||||
|
"Unknown data integrity issue with KYC resource.");
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -124,6 +124,7 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
|||||||
private final BusinessEventNotifierService businessEventNotifierService;
|
private final BusinessEventNotifierService businessEventNotifierService;
|
||||||
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
|
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
|
||||||
private final ExternalIdFactory externalIdFactory;
|
private final ExternalIdFactory externalIdFactory;
|
||||||
|
private final ClientKycWritePlatformService clientKycWritePlatformService;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
@@ -329,6 +330,16 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
|||||||
newClient.getId(), null, command.arrayOfParameterNamed(ClientApiConstants.datatables));
|
newClient.getId(), null, command.arrayOfParameterNamed(ClientApiConstants.datatables));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Inline KYC creation (optional) ───────────────────────────────
|
||||||
|
final JsonElement rootElement = this.fromApiJsonHelper.parse(command.json());
|
||||||
|
if (rootElement.getAsJsonObject().has(ClientApiConstants.kycParamName)) {
|
||||||
|
final JsonElement kycElement = rootElement.getAsJsonObject().get(ClientApiConstants.kycParamName);
|
||||||
|
if (!kycElement.isJsonNull()) {
|
||||||
|
this.clientKycWritePlatformService.initiateKycInline(newClient,
|
||||||
|
this.fromApiJsonHelper.toJson(kycElement));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
legalForm = LegalForm.fromInt(newClient.getLegalForm());
|
legalForm = LegalForm.fromInt(newClient.getLegalForm());
|
||||||
entityDatatableChecksWritePlatformService.runTheCheck(newClient.getId(), EntityTables.CLIENT.getName(),
|
entityDatatableChecksWritePlatformService.runTheCheck(newClient.getId(), EntityTables.CLIENT.getName(),
|
||||||
StatusEnum.CREATE.getValue(), EntityTables.CLIENT.getForeignKeyColumnNameOnDatatable(), legalForm.getLabel());
|
StatusEnum.CREATE.getValue(), EntityTables.CLIENT.getForeignKeyColumnNameOnDatatable(), legalForm.getLabel());
|
||||||
|
|||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.mobilelookup.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.mobilelookup.data.MobileLookupLogData;
|
||||||
|
import org.apache.fineract.portfolio.mobilelookup.data.MobileLookupResult;
|
||||||
|
import org.apache.fineract.portfolio.mobilelookup.domain.MobileLookupLog;
|
||||||
|
import org.apache.fineract.portfolio.mobilelookup.service.MobileLookupService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST endpoints for DIP / MNRL mobile number lookup.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* GET /v1/lookup/check?mobileNumber={mobileNumber} – Check mobile number
|
||||||
|
* GET /v1/lookup/logs – Get all lookup logs
|
||||||
|
* GET /v1/lookup/logs?mobileNumber={num}&status={status} – Filter logs
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Path("/v1/lookup")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Mobile Number Lookup",
|
||||||
|
description = "Check mobile numbers against DIP (Digital Intelligence Platform) suspected numbers "
|
||||||
|
+ "and MNRL (Mobile Number Revocation List) disconnected numbers. "
|
||||||
|
+ "Use during client registration to verify mobile numbers.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MobileLookupApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final MobileLookupService mobileLookupService;
|
||||||
|
private final DefaultToApiJsonSerializer<MobileLookupResult> resultSerializer;
|
||||||
|
private final DefaultToApiJsonSerializer<List> listSerializer;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/check")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Check mobile number against DIP and MNRL",
|
||||||
|
description = "Calls the external DIP/MNRL lookup service to check the given mobile number. "
|
||||||
|
+ "Returns status: CLEAN, SUSPECTED, DISCONNECTED, or SUSPECTED_AND_DISCONNECTED. "
|
||||||
|
+ "Every lookup is logged in the activity log.",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(responseCode = "200", description = "Mobile number lookup result",
|
||||||
|
content = @Content(schema = @Schema(implementation = MobileLookupResult.class)))
|
||||||
|
})
|
||||||
|
public String checkMobileNumber(
|
||||||
|
@QueryParam("mobileNumber") @Parameter(description = "Mobile number to check (10-15 digits)") final String mobileNumber) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission("MOBILE_LOOKUP");
|
||||||
|
final MobileLookupResult result = mobileLookupService.checkMobileNumber(mobileNumber);
|
||||||
|
return resultSerializer.serialize(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/logs")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Get mobile number lookup activity logs",
|
||||||
|
description = "Returns lookup activity logs. Optionally filter by mobile number or status "
|
||||||
|
+ "(CLEAN, SUSPECTED, DISCONNECTED, SUSPECTED_AND_DISCONNECTED, ERROR).",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(responseCode = "200", description = "List of lookup activity logs")
|
||||||
|
})
|
||||||
|
public String getLookupLogs(
|
||||||
|
@QueryParam("mobileNumber") @Parameter(description = "Filter by mobile number") final String mobileNumber,
|
||||||
|
@QueryParam("status") @Parameter(description = "Filter by status: CLEAN, SUSPECTED, DISCONNECTED, SUSPECTED_AND_DISCONNECTED, ERROR") final String status) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission("MOBILE_LOOKUP");
|
||||||
|
|
||||||
|
final List<MobileLookupLog> logs;
|
||||||
|
if (mobileNumber != null && !mobileNumber.isBlank()) {
|
||||||
|
logs = mobileLookupService.getLookupLogsByMobileNumber(mobileNumber.trim());
|
||||||
|
} else if (status != null && !status.isBlank()) {
|
||||||
|
logs = mobileLookupService.getLookupLogsByStatus(status.trim().toUpperCase());
|
||||||
|
} else {
|
||||||
|
logs = mobileLookupService.getLookupLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<MobileLookupLogData> logDataList = logs.stream()
|
||||||
|
.map(MobileLookupLogData::fromEntity)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return listSerializer.serialize(logDataList);
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.mobilelookup.data;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MobileLookupDetailData {
|
||||||
|
|
||||||
|
private final String dipStatus;
|
||||||
|
private final String reason;
|
||||||
|
private final String reportedBy;
|
||||||
|
|
||||||
|
private final String disconnectionReason;
|
||||||
|
private final String operator;
|
||||||
|
|
||||||
|
public static MobileLookupDetailData forSuspected(String dipStatus, String reason, String reportedBy) {
|
||||||
|
return new MobileLookupDetailData(dipStatus, reason, reportedBy, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MobileLookupDetailData forDisconnected(String disconnectionReason, String operator) {
|
||||||
|
return new MobileLookupDetailData(null, null, null, disconnectionReason, operator);
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.mobilelookup.data;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.apache.fineract.portfolio.mobilelookup.domain.MobileLookupLog;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MobileLookupLogData {
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final String mobileNumber;
|
||||||
|
private final String lookupStatus;
|
||||||
|
private final boolean suspected;
|
||||||
|
private final boolean disconnected;
|
||||||
|
private final String responseMessage;
|
||||||
|
private final String lookupSource;
|
||||||
|
private final Long clientId;
|
||||||
|
private final String lookedUpOn;
|
||||||
|
|
||||||
|
public static MobileLookupLogData fromEntity(final MobileLookupLog log) {
|
||||||
|
return new MobileLookupLogData(
|
||||||
|
log.getId(),
|
||||||
|
log.getMobileNumber(),
|
||||||
|
log.getLookupStatus(),
|
||||||
|
log.isSuspected(),
|
||||||
|
log.isDisconnected(),
|
||||||
|
log.getResponseMessage(),
|
||||||
|
log.getLookupSource(),
|
||||||
|
log.getClientId(),
|
||||||
|
log.getLookedUpOn() != null ? log.getLookedUpOn().toString() : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.mobilelookup.data;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MobileLookupResult {
|
||||||
|
|
||||||
|
private final boolean success;
|
||||||
|
private final String message;
|
||||||
|
private final MobileLookupData data;
|
||||||
|
private final String timestamp;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class MobileLookupData {
|
||||||
|
|
||||||
|
private final String mobileNumber;
|
||||||
|
private final String status;
|
||||||
|
private final boolean suspected;
|
||||||
|
private final boolean disconnected;
|
||||||
|
private final MobileLookupDetailData suspectedDetail;
|
||||||
|
private final MobileLookupDetailData disconnectedDetail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MobileLookupResult clean(String mobileNumber) {
|
||||||
|
MobileLookupData data = new MobileLookupData(mobileNumber, "CLEAN", false, false, null, null);
|
||||||
|
return new MobileLookupResult(true,
|
||||||
|
"Number is CLEAN — not found in DIP or MNRL records",
|
||||||
|
data, OffsetDateTime.now(ZoneOffset.UTC).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MobileLookupResult suspected(String mobileNumber, MobileLookupDetailData suspectedDetail) {
|
||||||
|
MobileLookupData data = new MobileLookupData(mobileNumber, "SUSPECTED", true, false, suspectedDetail, null);
|
||||||
|
return new MobileLookupResult(true,
|
||||||
|
"Number is SUSPECTED — found in DIP records",
|
||||||
|
data, OffsetDateTime.now(ZoneOffset.UTC).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MobileLookupResult disconnected(String mobileNumber, MobileLookupDetailData disconnectedDetail) {
|
||||||
|
MobileLookupData data = new MobileLookupData(mobileNumber, "DISCONNECTED", false, true, null, disconnectedDetail);
|
||||||
|
return new MobileLookupResult(true,
|
||||||
|
"Number is DISCONNECTED — found in MNRL records",
|
||||||
|
data, OffsetDateTime.now(ZoneOffset.UTC).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MobileLookupResult suspectedAndDisconnected(String mobileNumber,
|
||||||
|
MobileLookupDetailData suspectedDetail, MobileLookupDetailData disconnectedDetail) {
|
||||||
|
MobileLookupData data = new MobileLookupData(mobileNumber, "SUSPECTED_AND_DISCONNECTED",
|
||||||
|
true, true, suspectedDetail, disconnectedDetail);
|
||||||
|
return new MobileLookupResult(true,
|
||||||
|
"Number is SUSPECTED and DISCONNECTED — found in both DIP and MNRL records",
|
||||||
|
data, OffsetDateTime.now(ZoneOffset.UTC).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MobileLookupResult serviceUnavailable(String mobileNumber, String message) {
|
||||||
|
MobileLookupData data = new MobileLookupData(mobileNumber, "SERVICE_UNAVAILABLE", false, false, null, null);
|
||||||
|
return new MobileLookupResult(false, message, data, OffsetDateTime.now(ZoneOffset.UTC).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -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.mobilelookup.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.fineract.portfolio.mobilelookup.data.MobileLookupResult;
|
||||||
|
import org.apache.fineract.portfolio.mobilelookup.domain.MobileLookupLog;
|
||||||
|
|
||||||
|
public interface MobileLookupService {
|
||||||
|
|
||||||
|
MobileLookupResult checkMobileNumber(String mobileNumber);
|
||||||
|
|
||||||
|
MobileLookupResult checkMobileNumber(String mobileNumber, String source, Long clientId);
|
||||||
|
|
||||||
|
List<MobileLookupLog> getLookupLogs();
|
||||||
|
|
||||||
|
List<MobileLookupLog> getLookupLogsByMobileNumber(String mobileNumber);
|
||||||
|
|
||||||
|
List<MobileLookupLog> getLookupLogsByStatus(String status);
|
||||||
|
}
|
||||||
+219
@@ -0,0 +1,219 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.mobilelookup.service;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.apache.fineract.portfolio.mobilelookup.data.MobileLookupDetailData;
|
||||||
|
import org.apache.fineract.portfolio.mobilelookup.data.MobileLookupResult;
|
||||||
|
import org.apache.fineract.portfolio.mobilelookup.domain.MobileLookupLog;
|
||||||
|
import org.apache.fineract.portfolio.mobilelookup.domain.MobileLookupLogRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class MobileLookupServiceImpl implements MobileLookupService {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(MobileLookupServiceImpl.class);
|
||||||
|
|
||||||
|
/** Demo numbers that always return SUSPECTED without calling the external API. */
|
||||||
|
private static final Set<String> DEMO_SUSPECTED_NUMBERS = Set.of(
|
||||||
|
"9827123456", "9827456789", "9826234567",
|
||||||
|
"9826987654", "9893123456", "9893765432"
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Demo numbers that always return CLEAN without calling the external API. */
|
||||||
|
private static final Set<String> DEMO_CLEAN_NUMBERS = Set.of(
|
||||||
|
"9752123456", "9752345678", "9752987654"
|
||||||
|
);
|
||||||
|
|
||||||
|
private final String baseUrl;
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
private final MobileLookupLogRepository logRepository;
|
||||||
|
|
||||||
|
public MobileLookupServiceImpl(@Value("${fineract.mobile-lookup.base-url}") String baseUrl,
|
||||||
|
MobileLookupLogRepository logRepository) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
this.restTemplate = new RestTemplate();
|
||||||
|
this.logRepository = logRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public MobileLookupResult checkMobileNumber(final String mobileNumber) {
|
||||||
|
return checkMobileNumber(mobileNumber, "API", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public MobileLookupResult checkMobileNumber(final String mobileNumber, final String source, final Long clientId) {
|
||||||
|
if (mobileNumber == null || mobileNumber.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Mobile number must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
final String trimmed = mobileNumber.trim();
|
||||||
|
|
||||||
|
// Demo numbers — return SUSPECTED without calling the external API
|
||||||
|
if (isDemoSuspectedNumber(trimmed)) {
|
||||||
|
LOG.info("Demo suspected number detected: {}", trimmed);
|
||||||
|
final MobileLookupDetailData suspectedDetail = MobileLookupDetailData.forSuspected(
|
||||||
|
"active", "Demo DIP suspected number", "DEMO");
|
||||||
|
final MobileLookupResult result = MobileLookupResult.suspected(trimmed, suspectedDetail);
|
||||||
|
saveLog(trimmed, "SUSPECTED", true, false,
|
||||||
|
result.getMessage(), null, source, clientId);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demo numbers — return CLEAN without calling the external API
|
||||||
|
if (isDemoCleanNumber(trimmed)) {
|
||||||
|
LOG.info("Demo clean number detected: {}", trimmed);
|
||||||
|
final MobileLookupResult result = MobileLookupResult.clean(trimmed);
|
||||||
|
saveLog(trimmed, "CLEAN", false, false,
|
||||||
|
result.getMessage(), null, source, clientId);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String url = baseUrl + "/api/v1/lookup/check?mobileNumber=" + trimmed;
|
||||||
|
|
||||||
|
LOG.info("Calling DIP/MNRL lookup API: {}", url);
|
||||||
|
|
||||||
|
String rawResponse = null;
|
||||||
|
try {
|
||||||
|
rawResponse = restTemplate.getForObject(url, String.class);
|
||||||
|
LOG.info("DIP/MNRL lookup response for {}: {}", trimmed, rawResponse);
|
||||||
|
|
||||||
|
final MobileLookupResult result = parseResponse(rawResponse);
|
||||||
|
|
||||||
|
saveLog(trimmed, result.getData().getStatus(),
|
||||||
|
result.getData().isSuspected(), result.getData().isDisconnected(),
|
||||||
|
result.getMessage(), rawResponse, source, clientId);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Failed to call DIP/MNRL lookup API for {}: {}", trimmed, e.getMessage());
|
||||||
|
saveLog(trimmed, "ERROR", false, false,
|
||||||
|
e.getMessage(), rawResponse, source, clientId);
|
||||||
|
// Return CLEAN with service-unavailable message instead of throwing
|
||||||
|
return MobileLookupResult.serviceUnavailable(trimmed,
|
||||||
|
"DIP/MNRL lookup service is unavailable. Number treated as CLEAN by default.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<MobileLookupLog> getLookupLogs() {
|
||||||
|
return logRepository.findAllOrderByLookedUpOnDesc();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<MobileLookupLog> getLookupLogsByMobileNumber(final String mobileNumber) {
|
||||||
|
return logRepository.findByMobileNumber(mobileNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<MobileLookupLog> getLookupLogsByStatus(final String status) {
|
||||||
|
return logRepository.findByLookupStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveLog(final String mobileNumber, final String status,
|
||||||
|
final boolean suspected, final boolean disconnected,
|
||||||
|
final String message, final String rawResponse,
|
||||||
|
final String source, final Long clientId) {
|
||||||
|
try {
|
||||||
|
final MobileLookupLog log = new MobileLookupLog(
|
||||||
|
mobileNumber, status, suspected, disconnected,
|
||||||
|
message, rawResponse, source, clientId);
|
||||||
|
logRepository.save(log);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Failed to save mobile lookup log for '{}': {}", mobileNumber, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MobileLookupResult parseResponse(final String responseJson) {
|
||||||
|
final JsonObject root = JsonParser.parseString(responseJson).getAsJsonObject();
|
||||||
|
|
||||||
|
final boolean success = root.get("success").getAsBoolean();
|
||||||
|
final String message = root.get("message").getAsString();
|
||||||
|
final String timestamp = root.get("timestamp").getAsString();
|
||||||
|
|
||||||
|
final JsonObject data = root.getAsJsonObject("data");
|
||||||
|
final String mobileNumber = data.get("mobileNumber").getAsString();
|
||||||
|
final String status = data.get("status").getAsString();
|
||||||
|
final boolean suspected = data.get("suspected").getAsBoolean();
|
||||||
|
final boolean disconnected = data.get("disconnected").getAsBoolean();
|
||||||
|
|
||||||
|
MobileLookupDetailData suspectedDetail = null;
|
||||||
|
MobileLookupDetailData disconnectedDetail = null;
|
||||||
|
|
||||||
|
if (suspected && !data.get("suspectedDetail").isJsonNull()) {
|
||||||
|
JsonObject sd = data.getAsJsonObject("suspectedDetail");
|
||||||
|
suspectedDetail = MobileLookupDetailData.forSuspected(
|
||||||
|
getStringOrNull(sd, "dipStatus"),
|
||||||
|
getStringOrNull(sd, "reason"),
|
||||||
|
getStringOrNull(sd, "reportedBy"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disconnected && !data.get("disconnectedDetail").isJsonNull()) {
|
||||||
|
JsonObject dd = data.getAsJsonObject("disconnectedDetail");
|
||||||
|
disconnectedDetail = MobileLookupDetailData.forDisconnected(
|
||||||
|
getStringOrNull(dd, "disconnectionReason"),
|
||||||
|
getStringOrNull(dd, "operator"));
|
||||||
|
}
|
||||||
|
|
||||||
|
MobileLookupResult.MobileLookupData lookupData = new MobileLookupResult.MobileLookupData(
|
||||||
|
mobileNumber, status, suspected, disconnected, suspectedDetail, disconnectedDetail);
|
||||||
|
|
||||||
|
return new MobileLookupResult(success, message, lookupData, timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDemoCleanNumber(final String mobileNumber) {
|
||||||
|
if (DEMO_CLEAN_NUMBERS.contains(mobileNumber)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (mobileNumber.startsWith("91") && mobileNumber.length() > 10) {
|
||||||
|
return DEMO_CLEAN_NUMBERS.contains(mobileNumber.substring(2));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDemoSuspectedNumber(final String mobileNumber) {
|
||||||
|
// Check with and without country code prefix (91)
|
||||||
|
if (DEMO_SUSPECTED_NUMBERS.contains(mobileNumber)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (mobileNumber.startsWith("91") && mobileNumber.length() > 10) {
|
||||||
|
return DEMO_SUSPECTED_NUMBERS.contains(mobileNumber.substring(2));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getStringOrNull(JsonObject json, String key) {
|
||||||
|
JsonElement el = json.get(key);
|
||||||
|
return (el != null && !el.isJsonNull()) ? el.getAsString() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+194
@@ -0,0 +1,194 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.sanctions.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.Context;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.data.SanctionsScreeningLogData;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.data.SanctionsScreeningResult;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.domain.SanctionsScreeningLog;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.service.SanctionsScreeningService;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.service.SanctionsXmlLoaderService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST endpoints for sanctions screening against the UN Security Council consolidated list.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* GET /v1/sanctions/screen?name={name} – Screen a name against the sanctions list
|
||||||
|
* POST /v1/sanctions/screen – Screen a name (JSON body: {"name": "..."})
|
||||||
|
* POST /v1/sanctions/load – Load sanctions data from XML
|
||||||
|
* GET /v1/sanctions/status – Get sanctions database status (count)
|
||||||
|
* GET /v1/sanctions/logs – Get all screening activity logs
|
||||||
|
* GET /v1/sanctions/logs?result=FLAGGED – Filter logs by result
|
||||||
|
* GET /v1/sanctions/logs?name=searchTerm – Search logs by screened name
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Path("/v1/sanctions")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Sanctions Screening",
|
||||||
|
description = "Screen client names against the UN Security Council consolidated sanctions list. "
|
||||||
|
+ "Supports both individual and entity screening with full activity logging.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SanctionsScreeningApiResource {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(SanctionsScreeningApiResource.class);
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final SanctionsScreeningService sanctionsScreeningService;
|
||||||
|
private final SanctionsXmlLoaderService sanctionsXmlLoaderService;
|
||||||
|
private final DefaultToApiJsonSerializer<SanctionsScreeningResult> screeningResultSerializer;
|
||||||
|
private final DefaultToApiJsonSerializer<Map> mapSerializer;
|
||||||
|
private final DefaultToApiJsonSerializer<List> listSerializer;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/screen")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Screen a name against sanctions list",
|
||||||
|
description = "Checks the given name against the UN Security Council consolidated sanctions list. "
|
||||||
|
+ "Returns FLAGGED status with match details if found, or CLEAR status if no match. "
|
||||||
|
+ "Every screening is logged in the activity log.",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(responseCode = "200", description = "Screening result",
|
||||||
|
content = @Content(schema = @Schema(implementation = SanctionsScreeningResult.class)))
|
||||||
|
})
|
||||||
|
public String screenNameGet(
|
||||||
|
@QueryParam("name") @Parameter(description = "Name to screen against sanctions list") final String name) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission("SANCTIONS");
|
||||||
|
final SanctionsScreeningResult result = sanctionsScreeningService.screenName(name, "API", null, null);
|
||||||
|
return screeningResultSerializer.serialize(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/screen")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Screen a name against sanctions list (POST)",
|
||||||
|
description = "Accepts JSON body with 'name' field. Checks against the UN Security Council consolidated sanctions list. "
|
||||||
|
+ "Every screening is logged in the activity log.",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(responseCode = "200", description = "Screening result",
|
||||||
|
content = @Content(schema = @Schema(implementation = SanctionsScreeningResult.class)))
|
||||||
|
})
|
||||||
|
public String screenNamePost(final String apiRequestBodyAsJson) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission("SANCTIONS");
|
||||||
|
|
||||||
|
final com.google.gson.JsonObject json = com.google.gson.JsonParser.parseString(apiRequestBodyAsJson).getAsJsonObject();
|
||||||
|
final String name = json.has("name") ? json.get("name").getAsString() : null;
|
||||||
|
final String source = json.has("source") ? json.get("source").getAsString() : "CLIENT_CREATION";
|
||||||
|
final Long clientId = json.has("clientId") ? json.get("clientId").getAsLong() : null;
|
||||||
|
|
||||||
|
final SanctionsScreeningResult result = sanctionsScreeningService.screenName(name, source, clientId, null);
|
||||||
|
return screeningResultSerializer.serialize(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/load")
|
||||||
|
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_OCTET_STREAM })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Load sanctions data from UN XML",
|
||||||
|
description = "Parses the UN Security Council consolidated sanctions list XML and loads all "
|
||||||
|
+ "individuals and entities into the database.",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(responseCode = "200", description = "Load result with count of entries loaded")
|
||||||
|
})
|
||||||
|
public String loadSanctionsXml(final InputStream xmlInputStream) {
|
||||||
|
this.context.authenticatedUser().validateHasPermissionTo("LOAD_SANCTIONS");
|
||||||
|
|
||||||
|
LOG.info("Loading sanctions data from XML...");
|
||||||
|
final int count = sanctionsXmlLoaderService.loadFromXml(xmlInputStream);
|
||||||
|
|
||||||
|
final Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("message", "Sanctions data loaded successfully");
|
||||||
|
response.put("entriesLoaded", count);
|
||||||
|
response.put("totalActive", sanctionsScreeningService.getSanctionsCount());
|
||||||
|
return mapSerializer.serialize(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/status")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Get sanctions database status",
|
||||||
|
description = "Returns the count of active sanctions entries in the database.",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(responseCode = "200", description = "Sanctions database status")
|
||||||
|
})
|
||||||
|
public String getSanctionsStatus() {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission("SANCTIONS");
|
||||||
|
|
||||||
|
final Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("totalActiveEntries", sanctionsScreeningService.getSanctionsCount());
|
||||||
|
response.put("status", "ACTIVE");
|
||||||
|
return mapSerializer.serialize(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/logs")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Get sanctions screening activity logs",
|
||||||
|
description = "Returns the screening activity logs. Optionally filter by result (FLAGGED/CLEAR) or search by name.",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(responseCode = "200", description = "List of screening activity logs")
|
||||||
|
})
|
||||||
|
public String getScreeningLogs(
|
||||||
|
@QueryParam("result") @Parameter(description = "Filter by result: FLAGGED or CLEAR") final String result,
|
||||||
|
@QueryParam("name") @Parameter(description = "Search logs by screened name") final String name) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission("SANCTIONS");
|
||||||
|
|
||||||
|
final List<SanctionsScreeningLog> logs;
|
||||||
|
if (name != null && !name.isBlank()) {
|
||||||
|
logs = sanctionsScreeningService.searchScreeningLogs(name);
|
||||||
|
} else if (result != null && !result.isBlank()) {
|
||||||
|
logs = sanctionsScreeningService.getScreeningLogsByResult(result.toUpperCase());
|
||||||
|
} else {
|
||||||
|
logs = sanctionsScreeningService.getScreeningLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<SanctionsScreeningLogData> logDataList = logs.stream()
|
||||||
|
.map(SanctionsScreeningLogData::fromEntity)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return listSerializer.serialize(logDataList);
|
||||||
|
}
|
||||||
|
}
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.sanctions.data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data for a single sanctions match returned in the screening result.
|
||||||
|
*/
|
||||||
|
public final class SanctionsMatchData {
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final String dataId;
|
||||||
|
private final String entryType;
|
||||||
|
private final String fullName;
|
||||||
|
private final String unListType;
|
||||||
|
private final String referenceNumber;
|
||||||
|
private final LocalDate listedOn;
|
||||||
|
private final String gender;
|
||||||
|
private final String nationality;
|
||||||
|
private final String dateOfBirth;
|
||||||
|
private final String placeOfBirth;
|
||||||
|
private final String comments;
|
||||||
|
private final List<String> aliases;
|
||||||
|
|
||||||
|
public SanctionsMatchData(final Long id, final String dataId, final String entryType,
|
||||||
|
final String fullName, final String unListType, final String referenceNumber,
|
||||||
|
final LocalDate listedOn, final String gender, final String nationality,
|
||||||
|
final String dateOfBirth, final String placeOfBirth, final String comments,
|
||||||
|
final List<String> aliases) {
|
||||||
|
this.id = id;
|
||||||
|
this.dataId = dataId;
|
||||||
|
this.entryType = entryType;
|
||||||
|
this.fullName = fullName;
|
||||||
|
this.unListType = unListType;
|
||||||
|
this.referenceNumber = referenceNumber;
|
||||||
|
this.listedOn = listedOn;
|
||||||
|
this.gender = gender;
|
||||||
|
this.nationality = nationality;
|
||||||
|
this.dateOfBirth = dateOfBirth;
|
||||||
|
this.placeOfBirth = placeOfBirth;
|
||||||
|
this.comments = comments;
|
||||||
|
this.aliases = aliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public String getDataId() { return dataId; }
|
||||||
|
public String getEntryType() { return entryType; }
|
||||||
|
public String getFullName() { return fullName; }
|
||||||
|
public String getUnListType() { return unListType; }
|
||||||
|
public String getReferenceNumber() { return referenceNumber; }
|
||||||
|
public LocalDate getListedOn() { return listedOn; }
|
||||||
|
public String getGender() { return gender; }
|
||||||
|
public String getNationality() { return nationality; }
|
||||||
|
public String getDateOfBirth() { return dateOfBirth; }
|
||||||
|
public String getPlaceOfBirth() { return placeOfBirth; }
|
||||||
|
public String getComments() { return comments; }
|
||||||
|
public List<String> getAliases() { return aliases; }
|
||||||
|
}
|
||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.sanctions.data;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.domain.SanctionsScreeningLog;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only data transfer object for sanctions screening activity log entries.
|
||||||
|
*/
|
||||||
|
public final class SanctionsScreeningLogData {
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final String screenedName;
|
||||||
|
private final String screeningResult;
|
||||||
|
private final Integer matchCount;
|
||||||
|
private final String matchedEntryIds;
|
||||||
|
private final String matchedNames;
|
||||||
|
private final String screeningSource;
|
||||||
|
private final Long clientId;
|
||||||
|
private final String ipAddress;
|
||||||
|
private final OffsetDateTime screenedOn;
|
||||||
|
private final Long screenedBy;
|
||||||
|
|
||||||
|
public SanctionsScreeningLogData(final Long id, final String screenedName, final String screeningResult,
|
||||||
|
final Integer matchCount, final String matchedEntryIds, final String matchedNames,
|
||||||
|
final String screeningSource, final Long clientId, final String ipAddress,
|
||||||
|
final OffsetDateTime screenedOn, final Long screenedBy) {
|
||||||
|
this.id = id;
|
||||||
|
this.screenedName = screenedName;
|
||||||
|
this.screeningResult = screeningResult;
|
||||||
|
this.matchCount = matchCount;
|
||||||
|
this.matchedEntryIds = matchedEntryIds;
|
||||||
|
this.matchedNames = matchedNames;
|
||||||
|
this.screeningSource = screeningSource;
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.ipAddress = ipAddress;
|
||||||
|
this.screenedOn = screenedOn;
|
||||||
|
this.screenedBy = screenedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SanctionsScreeningLogData fromEntity(final SanctionsScreeningLog log) {
|
||||||
|
return new SanctionsScreeningLogData(
|
||||||
|
log.getId(),
|
||||||
|
log.getScreenedName(),
|
||||||
|
log.getScreeningResult(),
|
||||||
|
log.getMatchCount(),
|
||||||
|
log.getMatchedEntryIds(),
|
||||||
|
log.getMatchedNames(),
|
||||||
|
log.getScreeningSource(),
|
||||||
|
log.getClientId(),
|
||||||
|
log.getIpAddress(),
|
||||||
|
log.getScreenedOn(),
|
||||||
|
log.getCreatedBy().orElse(null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public String getScreenedName() { return screenedName; }
|
||||||
|
public String getScreeningResult() { return screeningResult; }
|
||||||
|
public Integer getMatchCount() { return matchCount; }
|
||||||
|
public String getMatchedEntryIds() { return matchedEntryIds; }
|
||||||
|
public String getMatchedNames() { return matchedNames; }
|
||||||
|
public String getScreeningSource() { return screeningSource; }
|
||||||
|
public Long getClientId() { return clientId; }
|
||||||
|
public String getIpAddress() { return ipAddress; }
|
||||||
|
public OffsetDateTime getScreenedOn() { return screenedOn; }
|
||||||
|
public Long getScreenedBy() { return screenedBy; }
|
||||||
|
}
|
||||||
+66
@@ -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.sanctions.data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response object for sanctions screening API.
|
||||||
|
*/
|
||||||
|
public final class SanctionsScreeningResult {
|
||||||
|
|
||||||
|
private final String screenedName;
|
||||||
|
private final boolean matchFound;
|
||||||
|
private final String status;
|
||||||
|
private final String message;
|
||||||
|
private final int matchCount;
|
||||||
|
private final List<SanctionsMatchData> matches;
|
||||||
|
|
||||||
|
private SanctionsScreeningResult(final String screenedName, final boolean matchFound,
|
||||||
|
final String status, final String message, final int matchCount,
|
||||||
|
final List<SanctionsMatchData> matches) {
|
||||||
|
this.screenedName = screenedName;
|
||||||
|
this.matchFound = matchFound;
|
||||||
|
this.status = status;
|
||||||
|
this.message = message;
|
||||||
|
this.matchCount = matchCount;
|
||||||
|
this.matches = matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SanctionsScreeningResult matched(final String screenedName,
|
||||||
|
final List<SanctionsMatchData> matches) {
|
||||||
|
return new SanctionsScreeningResult(screenedName, true, "FLAGGED",
|
||||||
|
"Potential match found in UN Security Council consolidated sanctions list. "
|
||||||
|
+ "Manual review required before proceeding.",
|
||||||
|
matches.size(), matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SanctionsScreeningResult clear(final String screenedName) {
|
||||||
|
return new SanctionsScreeningResult(screenedName, false, "CLEAR",
|
||||||
|
"No match found in UN Security Council consolidated sanctions list.",
|
||||||
|
0, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getScreenedName() { return screenedName; }
|
||||||
|
public boolean isMatchFound() { return matchFound; }
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public String getMessage() { return message; }
|
||||||
|
public int getMatchCount() { return matchCount; }
|
||||||
|
public List<SanctionsMatchData> getMatches() { return matches; }
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.sanctions.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.data.SanctionsScreeningResult;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.domain.SanctionsScreeningLog;
|
||||||
|
|
||||||
|
public interface SanctionsScreeningService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Screen a client name against the UN consolidated sanctions list.
|
||||||
|
* Logs the activity automatically.
|
||||||
|
*/
|
||||||
|
SanctionsScreeningResult screenName(String name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Screen a client name with source context for activity logging.
|
||||||
|
*
|
||||||
|
* @param name the name to screen
|
||||||
|
* @param source screening source (CLIENT_CREATION, MANUAL_CHECK, API)
|
||||||
|
* @param clientId optional client ID if screening during client onboarding
|
||||||
|
* @param ipAddress optional IP address of the requester
|
||||||
|
*/
|
||||||
|
SanctionsScreeningResult screenName(String name, String source, Long clientId, String ipAddress);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total count of active sanctions entries.
|
||||||
|
*/
|
||||||
|
long getSanctionsCount();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all screening activity logs (most recent first).
|
||||||
|
*/
|
||||||
|
List<SanctionsScreeningLog> getScreeningLogs();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get screening logs filtered by result (FLAGGED or CLEAR).
|
||||||
|
*/
|
||||||
|
List<SanctionsScreeningLog> getScreeningLogsByResult(String result);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search screening logs by screened name.
|
||||||
|
*/
|
||||||
|
List<SanctionsScreeningLog> searchScreeningLogs(String name);
|
||||||
|
}
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.sanctions.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.data.SanctionsMatchData;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.data.SanctionsScreeningResult;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.domain.SanctionsAlias;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.domain.SanctionsEntry;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.domain.SanctionsEntryRepository;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.domain.SanctionsScreeningLog;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.domain.SanctionsScreeningLogRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SanctionsScreeningServiceImpl implements SanctionsScreeningService {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(SanctionsScreeningServiceImpl.class);
|
||||||
|
|
||||||
|
private final SanctionsEntryRepository sanctionsEntryRepository;
|
||||||
|
private final SanctionsScreeningLogRepository screeningLogRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public SanctionsScreeningResult screenName(final String name) {
|
||||||
|
return screenName(name, "API", null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public SanctionsScreeningResult screenName(final String name, final String source,
|
||||||
|
final Long clientId, final String ipAddress) {
|
||||||
|
if (name == null || name.isBlank()) {
|
||||||
|
saveLog(name, "CLEAR", 0, null, null, source, clientId, ipAddress);
|
||||||
|
return SanctionsScreeningResult.clear(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String trimmedName = name.trim();
|
||||||
|
LOG.info("Screening name against sanctions list: {}", trimmedName);
|
||||||
|
|
||||||
|
final List<SanctionsEntry> matches = sanctionsEntryRepository.searchByNameOrAlias(trimmedName);
|
||||||
|
|
||||||
|
if (matches.isEmpty()) {
|
||||||
|
LOG.info("No sanctions match found for: {}", trimmedName);
|
||||||
|
saveLog(trimmedName, "CLEAR", 0, null, null, source, clientId, ipAddress);
|
||||||
|
return SanctionsScreeningResult.clear(trimmedName);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.warn("Sanctions match found for '{}': {} potential match(es)", trimmedName, matches.size());
|
||||||
|
|
||||||
|
final List<SanctionsMatchData> matchDataList = matches.stream()
|
||||||
|
.map(this::toMatchData)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// Build matched IDs and names for the log
|
||||||
|
final String matchedEntryIds = matches.stream()
|
||||||
|
.map(e -> String.valueOf(e.getId()))
|
||||||
|
.collect(Collectors.joining(","));
|
||||||
|
final String matchedNames = matches.stream()
|
||||||
|
.map(SanctionsEntry::getFullName)
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
|
||||||
|
saveLog(trimmedName, "FLAGGED", matches.size(), matchedEntryIds, matchedNames, source, clientId, ipAddress);
|
||||||
|
|
||||||
|
return SanctionsScreeningResult.matched(trimmedName, matchDataList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public long getSanctionsCount() {
|
||||||
|
return sanctionsEntryRepository.countByActiveTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<SanctionsScreeningLog> getScreeningLogs() {
|
||||||
|
return screeningLogRepository.findAllOrderByScreenedOnDesc();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<SanctionsScreeningLog> getScreeningLogsByResult(final String result) {
|
||||||
|
return screeningLogRepository.findByScreeningResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<SanctionsScreeningLog> searchScreeningLogs(final String name) {
|
||||||
|
return screeningLogRepository.findByScreenedNameContaining(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveLog(final String screenedName, final String result, final int matchCount,
|
||||||
|
final String matchedEntryIds, final String matchedNames, final String source,
|
||||||
|
final Long clientId, final String ipAddress) {
|
||||||
|
try {
|
||||||
|
final SanctionsScreeningLog log = new SanctionsScreeningLog(
|
||||||
|
screenedName, result, matchCount, matchedEntryIds, matchedNames,
|
||||||
|
source, clientId, ipAddress);
|
||||||
|
screeningLogRepository.save(log);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Failed to save sanctions screening log for '{}': {}", screenedName, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SanctionsMatchData toMatchData(final SanctionsEntry entry) {
|
||||||
|
final List<String> aliases = entry.getAliases().stream()
|
||||||
|
.map(SanctionsAlias::getAliasName)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return new SanctionsMatchData(
|
||||||
|
entry.getId(),
|
||||||
|
entry.getDataId(),
|
||||||
|
entry.getEntryType(),
|
||||||
|
entry.getFullName(),
|
||||||
|
entry.getUnListType(),
|
||||||
|
entry.getReferenceNumber(),
|
||||||
|
entry.getListedOn(),
|
||||||
|
entry.getGender(),
|
||||||
|
entry.getNationality(),
|
||||||
|
entry.getDateOfBirth(),
|
||||||
|
entry.getPlaceOfBirth(),
|
||||||
|
entry.getComments(),
|
||||||
|
aliases
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+276
@@ -0,0 +1,276 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.sanctions.service;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.domain.SanctionsAlias;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.domain.SanctionsEntry;
|
||||||
|
import org.apache.fineract.portfolio.sanctions.domain.SanctionsEntryRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the UN Security Council consolidated sanctions list XML and loads entries into the database.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SanctionsXmlLoaderService {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(SanctionsXmlLoaderService.class);
|
||||||
|
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
|
private final SanctionsEntryRepository sanctionsEntryRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load sanctions data from XML input stream.
|
||||||
|
* Parses both INDIVIDUALS and ENTITIES sections.
|
||||||
|
*
|
||||||
|
* @param xmlInputStream the XML input stream containing the UN consolidated sanctions list
|
||||||
|
* @return count of entries loaded
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public int loadFromXml(final InputStream xmlInputStream) {
|
||||||
|
try {
|
||||||
|
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
|
// Disable external entities for security
|
||||||
|
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||||
|
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
||||||
|
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
||||||
|
final DocumentBuilder builder = factory.newDocumentBuilder();
|
||||||
|
final Document document = builder.parse(xmlInputStream);
|
||||||
|
document.getDocumentElement().normalize();
|
||||||
|
|
||||||
|
final List<SanctionsEntry> allEntries = new ArrayList<>();
|
||||||
|
|
||||||
|
// Parse INDIVIDUALS
|
||||||
|
final NodeList individuals = document.getElementsByTagName("INDIVIDUAL");
|
||||||
|
LOG.info("Found {} INDIVIDUAL entries in XML", individuals.getLength());
|
||||||
|
for (int i = 0; i < individuals.getLength(); i++) {
|
||||||
|
final Element element = (Element) individuals.item(i);
|
||||||
|
final SanctionsEntry entry = parseIndividual(element);
|
||||||
|
if (entry != null) {
|
||||||
|
allEntries.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse ENTITIES
|
||||||
|
final NodeList entities = document.getElementsByTagName("ENTITY");
|
||||||
|
LOG.info("Found {} ENTITY entries in XML", entities.getLength());
|
||||||
|
for (int i = 0; i < entities.getLength(); i++) {
|
||||||
|
final Element element = (Element) entities.item(i);
|
||||||
|
final SanctionsEntry entry = parseEntity(element);
|
||||||
|
if (entry != null) {
|
||||||
|
allEntries.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save in batches
|
||||||
|
final int batchSize = 100;
|
||||||
|
for (int i = 0; i < allEntries.size(); i += batchSize) {
|
||||||
|
final int end = Math.min(i + batchSize, allEntries.size());
|
||||||
|
sanctionsEntryRepository.saveAll(allEntries.subList(i, end));
|
||||||
|
sanctionsEntryRepository.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.info("Successfully loaded {} sanctions entries into database", allEntries.size());
|
||||||
|
return allEntries.size();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Failed to parse sanctions XML", e);
|
||||||
|
throw new RuntimeException("Failed to load sanctions data from XML: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SanctionsEntry parseIndividual(final Element element) {
|
||||||
|
try {
|
||||||
|
final String dataId = getTextContent(element, "DATAID");
|
||||||
|
final String firstName = getTextContent(element, "FIRST_NAME");
|
||||||
|
final String secondName = getTextContent(element, "SECOND_NAME");
|
||||||
|
final String thirdName = getTextContent(element, "THIRD_NAME");
|
||||||
|
final String fourthName = getTextContent(element, "FOURTH_NAME");
|
||||||
|
final String unListType = getTextContent(element, "UN_LIST_TYPE");
|
||||||
|
final String referenceNumber = getTextContent(element, "REFERENCE_NUMBER");
|
||||||
|
final LocalDate listedOn = parseDate(getTextContent(element, "LISTED_ON"));
|
||||||
|
final String gender = getTextContent(element, "GENDER");
|
||||||
|
final String comments = getTextContent(element, "COMMENTS1");
|
||||||
|
final String designation = getTextContent(element, "DESIGNATION");
|
||||||
|
|
||||||
|
// Collect nationality from NATIONALITY elements
|
||||||
|
final String nationality = collectNationality(element, "NATIONALITY");
|
||||||
|
|
||||||
|
// Collect date of birth info
|
||||||
|
final String dateOfBirth = collectDateOfBirth(element);
|
||||||
|
|
||||||
|
// Collect place of birth info
|
||||||
|
final String placeOfBirth = collectPlaceOfBirth(element);
|
||||||
|
|
||||||
|
final SanctionsEntry entry = new SanctionsEntry(dataId, "INDIVIDUAL", firstName,
|
||||||
|
secondName, thirdName, fourthName, unListType, referenceNumber, listedOn,
|
||||||
|
gender, nationality, comments, designation, dateOfBirth, placeOfBirth);
|
||||||
|
|
||||||
|
// Parse aliases
|
||||||
|
final NodeList aliasList = element.getElementsByTagName("INDIVIDUAL_ALIAS");
|
||||||
|
for (int i = 0; i < aliasList.getLength(); i++) {
|
||||||
|
final Element aliasElement = (Element) aliasList.item(i);
|
||||||
|
final String quality = getTextContent(aliasElement, "QUALITY");
|
||||||
|
final String aliasName = getTextContent(aliasElement, "ALIAS_NAME");
|
||||||
|
if (aliasName != null && !aliasName.isBlank()) {
|
||||||
|
entry.addAlias(new SanctionsAlias(quality, aliasName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.warn("Failed to parse INDIVIDUAL element, skipping", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SanctionsEntry parseEntity(final Element element) {
|
||||||
|
try {
|
||||||
|
final String dataId = getTextContent(element, "DATAID");
|
||||||
|
final String firstName = getTextContent(element, "FIRST_NAME");
|
||||||
|
final String unListType = getTextContent(element, "UN_LIST_TYPE");
|
||||||
|
final String referenceNumber = getTextContent(element, "REFERENCE_NUMBER");
|
||||||
|
final LocalDate listedOn = parseDate(getTextContent(element, "LISTED_ON"));
|
||||||
|
final String comments = getTextContent(element, "COMMENTS1");
|
||||||
|
|
||||||
|
final SanctionsEntry entry = new SanctionsEntry(dataId, "ENTITY", firstName,
|
||||||
|
null, null, null, unListType, referenceNumber, listedOn,
|
||||||
|
null, null, comments, null, null, null);
|
||||||
|
|
||||||
|
// Parse entity aliases
|
||||||
|
final NodeList aliasList = element.getElementsByTagName("ENTITY_ALIAS");
|
||||||
|
for (int i = 0; i < aliasList.getLength(); i++) {
|
||||||
|
final Element aliasElement = (Element) aliasList.item(i);
|
||||||
|
final String quality = getTextContent(aliasElement, "QUALITY");
|
||||||
|
final String aliasName = getTextContent(aliasElement, "ALIAS_NAME");
|
||||||
|
if (aliasName != null && !aliasName.isBlank()) {
|
||||||
|
entry.addAlias(new SanctionsAlias(quality, aliasName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.warn("Failed to parse ENTITY element, skipping", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTextContent(final Element parent, final String tagName) {
|
||||||
|
final NodeList nodeList = parent.getElementsByTagName(tagName);
|
||||||
|
if (nodeList.getLength() > 0) {
|
||||||
|
final String text = nodeList.item(0).getTextContent();
|
||||||
|
return (text != null && !text.isBlank()) ? text.trim() : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDate parseDate(final String dateStr) {
|
||||||
|
if (dateStr == null || dateStr.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return LocalDate.parse(dateStr, DATE_FORMAT);
|
||||||
|
} catch (DateTimeParseException e) {
|
||||||
|
// Try other common formats
|
||||||
|
try {
|
||||||
|
return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("dd/MM/yyyy"));
|
||||||
|
} catch (DateTimeParseException e2) {
|
||||||
|
LOG.debug("Could not parse date: {}", dateStr);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String collectNationality(final Element parent, final String tagName) {
|
||||||
|
final NodeList nodes = parent.getElementsByTagName(tagName);
|
||||||
|
if (nodes.getLength() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < nodes.getLength(); i++) {
|
||||||
|
final Element natElem = (Element) nodes.item(i);
|
||||||
|
final String value = getTextContent(natElem, "VALUE");
|
||||||
|
if (value != null) {
|
||||||
|
if (sb.length() > 0) { sb.append(", "); }
|
||||||
|
sb.append(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.length() > 0 ? sb.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String collectDateOfBirth(final Element parent) {
|
||||||
|
final NodeList nodes = parent.getElementsByTagName("INDIVIDUAL_DATE_OF_BIRTH");
|
||||||
|
if (nodes.getLength() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < nodes.getLength(); i++) {
|
||||||
|
final Element dobElem = (Element) nodes.item(i);
|
||||||
|
final String type = getTextContent(dobElem, "TYPE_OF_DATE");
|
||||||
|
final String date = getTextContent(dobElem, "DATE");
|
||||||
|
final String year = getTextContent(dobElem, "YEAR");
|
||||||
|
if (sb.length() > 0) { sb.append("; "); }
|
||||||
|
if (date != null) {
|
||||||
|
sb.append(date);
|
||||||
|
} else if (year != null) {
|
||||||
|
sb.append(year);
|
||||||
|
}
|
||||||
|
if (type != null) {
|
||||||
|
sb.append(" (").append(type).append(")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.length() > 0 ? sb.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String collectPlaceOfBirth(final Element parent) {
|
||||||
|
final NodeList nodes = parent.getElementsByTagName("INDIVIDUAL_PLACE_OF_BIRTH");
|
||||||
|
if (nodes.getLength() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < nodes.getLength(); i++) {
|
||||||
|
final Element pobElem = (Element) nodes.item(i);
|
||||||
|
final String city = getTextContent(pobElem, "CITY");
|
||||||
|
final String country = getTextContent(pobElem, "COUNTRY");
|
||||||
|
final String state = getTextContent(pobElem, "STATE_PROVINCE");
|
||||||
|
if (sb.length() > 0) { sb.append("; "); }
|
||||||
|
List<String> parts = new ArrayList<>();
|
||||||
|
if (city != null) { parts.add(city); }
|
||||||
|
if (state != null) { parts.add(state); }
|
||||||
|
if (country != null) { parts.add(country); }
|
||||||
|
sb.append(String.join(", ", parts));
|
||||||
|
}
|
||||||
|
return sb.length() > 0 ? sb.toString() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,9 +42,9 @@ fineract.security.oauth2.client.registrations.frontend-client.redirect-uris=${FI
|
|||||||
fineract.security.oauth2.client.registrations.frontend-client.require-authorization-consent=${FINERACT_SECURITY_OAUTH2_CLIENTS_FRONTEND_CONSENT:false}
|
fineract.security.oauth2.client.registrations.frontend-client.require-authorization-consent=${FINERACT_SECURITY_OAUTH2_CLIENTS_FRONTEND_CONSENT:false}
|
||||||
|
|
||||||
fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost}
|
fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost}
|
||||||
fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:3306}
|
fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432}
|
||||||
fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:root}
|
fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres}
|
||||||
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:mysql}
|
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:root}
|
||||||
fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:}
|
fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:}
|
||||||
fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
|
fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
|
||||||
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
|
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
|
||||||
@@ -402,10 +402,10 @@ server.tomcat.max-keep-alive-requests=${FINERACT_SERVER_TOMCAT_MAX_KEEP_ALIVE_RE
|
|||||||
server.tomcat.threads.max=${FINERACT_SERVER_TOMCAT_THREADS_MAX:200}
|
server.tomcat.threads.max=${FINERACT_SERVER_TOMCAT_THREADS_MAX:200}
|
||||||
server.tomcat.threads.min-spare=${FINERACT_SERVER_TOMCAT_THREADS_MIN_SPARE:10}
|
server.tomcat.threads.min-spare=${FINERACT_SERVER_TOMCAT_THREADS_MIN_SPARE:10}
|
||||||
server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABLED:false}
|
server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABLED:false}
|
||||||
spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.mariadb.jdbc.Driver}
|
spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver}
|
||||||
spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:mariadb://localhost:3306/fineract_tenants}
|
spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants}
|
||||||
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:root}
|
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres}
|
||||||
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:mysql}
|
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:root}
|
||||||
spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
|
spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
|
||||||
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
|
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
|
||||||
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
|
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
|
||||||
@@ -640,3 +640,25 @@ resilience4j.retry.instances.commandAuditFallback.wait-duration=${FINERACT_COMMA
|
|||||||
resilience4j.retry.instances.commandAuditFallback.enable-exponential-backoff=${FINERACT_COMMAND_AUDIT_FALLBACK_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
|
resilience4j.retry.instances.commandAuditFallback.enable-exponential-backoff=${FINERACT_COMMAND_AUDIT_FALLBACK_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
|
||||||
resilience4j.retry.instances.commandAuditFallback.exponential-backoff-multiplier=${FINERACT_COMMAND_AUDIT_FALLBACK_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
|
resilience4j.retry.instances.commandAuditFallback.exponential-backoff-multiplier=${FINERACT_COMMAND_AUDIT_FALLBACK_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
|
||||||
resilience4j.retry.instances.commandAuditFallback.retryExceptions=${FINERACT_COMMAND_AUDIT_FALLBACK_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
|
resilience4j.retry.instances.commandAuditFallback.retryExceptions=${FINERACT_COMMAND_AUDIT_FALLBACK_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
|
||||||
|
|
||||||
|
|
||||||
|
# ── LincPay eKYC API ──────────────────────────────────────────────────────────
|
||||||
|
# Set FINERACT_KYC_LINCPAY_API_KEY and FINERACT_KYC_LINCPAY_SECRET as env vars
|
||||||
|
# in production. Never commit real credentials to source control.
|
||||||
|
fineract.kyc.lincpay.api-key=${FINERACT_KYC_LINCPAY_API_KEY:api_7c9e3a1f6d4b2a8c5e1f3d7b9a2c6e4f}
|
||||||
|
fineract.kyc.lincpay.secret=${FINERACT_KYC_LINCPAY_SECRET:secret_4e8a2d7c1f9b3a6e5d2c8f1a7b4e9c3d}
|
||||||
|
fineract.kyc.lincpay.base-url=${FINERACT_KYC_LINCPAY_BASE_URL:https://ekyc.lincpay.in/EKYC/ekyc}
|
||||||
|
fineract.kyc.lincpay.path.aadhaar-generate-otp=${FINERACT_KYC_LINCPAY_PATH_AADHAAR_GENERATE_OTP:/aadhaar/generate-otp}
|
||||||
|
fineract.kyc.lincpay.path.aadhaar-verify-otp=${FINERACT_KYC_LINCPAY_PATH_AADHAAR_VERIFY_OTP:/aadhaar/verify-otp}
|
||||||
|
fineract.kyc.lincpay.path.pan-validate=${FINERACT_KYC_LINCPAY_PATH_PAN_VALIDATE:/validate-pan}
|
||||||
|
fineract.kyc.lincpay.path.gstin-validate=${FINERACT_KYC_LINCPAY_PATH_GSTIN_VALIDATE:/validate-gstn}
|
||||||
|
|
||||||
|
# ── OpenProject Integration ──────────────────────────────────────────────────
|
||||||
|
# Set FINERACT_OPENPROJECT_API_TOKEN as env var in production.
|
||||||
|
fineract.openproject.enabled=${FINERACT_OPENPROJECT_ENABLED:true}
|
||||||
|
fineract.openproject.base-url=${FINERACT_OPENPROJECT_BASE_URL:https://Sm.godemo.in}
|
||||||
|
fineract.openproject.project-id=${FINERACT_OPENPROJECT_PROJECT_ID:cbsnexora}
|
||||||
|
fineract.openproject.api-token=${FINERACT_OPENPROJECT_API_TOKEN:57dd9f933f113c7274e3ed3d5ca82e1e45e040c857160975087694f4d39ff596}
|
||||||
|
|
||||||
|
# ── DIP / MNRL Mobile Number Lookup ──────────────────────────────────────────
|
||||||
|
fineract.mobile-lookup.base-url=${FINERACT_MOBILE_LOOKUP_BASE_URL:http://localhost:8080}
|
||||||
|
|||||||
+336
@@ -0,0 +1,336 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<!--
|
||||||
|
KYC module – compliant with RBI Master Direction on KYC (updated 2023).
|
||||||
|
|
||||||
|
Tables created:
|
||||||
|
m_client_kyc – one KYC record per client
|
||||||
|
m_client_kyc_verification_log – 3rd-party API call audit trail
|
||||||
|
|
||||||
|
Permissions inserted:
|
||||||
|
READ_CLIENTKYC, CREATE_CLIENTKYC, UPDATE_CLIENTKYC,
|
||||||
|
VERIFY_CLIENTKYC, REJECT_CLIENTKYC
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
MySQL – m_client_kyc
|
||||||
|
═══════════════════════════════════════════════════════════════════ -->
|
||||||
|
<changeSet author="fineract" id="1" context="mysql">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_client_kyc"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_client_kyc">
|
||||||
|
<column autoIncrement="true" name="id" type="BIGINT">
|
||||||
|
<constraints nullable="false" primaryKey="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="client_id" type="BIGINT">
|
||||||
|
<constraints nullable="false" unique="true" uniqueConstraintName="uq_m_client_kyc_client_id"/>
|
||||||
|
</column>
|
||||||
|
<!-- kyc_status: 100=PENDING 200=IN_PROGRESS 300=VERIFIED 400=REJECTED 500=EXPIRED -->
|
||||||
|
<!-- INTEGER used (not SMALLINT) to match Java Integer JPA default mapping -->
|
||||||
|
<column name="kyc_status" type="INTEGER" defaultValueNumeric="100">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<!-- kyc_type: 1=INDIVIDUAL 2=NON_INDIVIDUAL -->
|
||||||
|
<column name="kyc_type" type="INTEGER">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<!-- risk_category: 1=LOW(10yr) 2=MEDIUM(8yr) 3=HIGH(2yr) -->
|
||||||
|
<column name="risk_category" type="INTEGER" defaultValueNumeric="1">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<!-- PAN (Permanent Account Number – Income Tax Dept) -->
|
||||||
|
<column name="pan_number" type="VARCHAR(10)"/>
|
||||||
|
<column name="pan_verified" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="pan_verified_on" type="DATE"/>
|
||||||
|
|
||||||
|
<!-- Aadhaar – stored masked per UIDAI directive (only last 4 digits) -->
|
||||||
|
<column name="aadhaar_number_masked" type="VARCHAR(12)"/>
|
||||||
|
<column name="aadhaar_verified" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="aadhaar_verified_on" type="DATE"/>
|
||||||
|
|
||||||
|
<!-- GSTIN – mandatory for NON_INDIVIDUAL clients -->
|
||||||
|
<column name="gstin_number" type="VARCHAR(15)"/>
|
||||||
|
<column name="gstin_verified" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="gstin_verified_on" type="DATE"/>
|
||||||
|
|
||||||
|
<!-- CKYCRR – Central KYC Records Registry number (CERSAI) -->
|
||||||
|
<column name="ckyc_number" type="VARCHAR(14)"/>
|
||||||
|
|
||||||
|
<!-- Lifecycle timestamps -->
|
||||||
|
<column name="initiated_on" type="DATE"/>
|
||||||
|
<column name="verified_on" type="DATE"/>
|
||||||
|
<column name="rejected_on" type="DATE"/>
|
||||||
|
<!-- expiry_date: derived from risk_category * verified_on year offset -->
|
||||||
|
<column name="expiry_date" type="DATE"/>
|
||||||
|
<column name="remarks" type="VARCHAR(500)"/>
|
||||||
|
|
||||||
|
<!-- Audit fields (AbstractAuditableWithUTCDateTimeCustom) -->
|
||||||
|
<column name="created_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_on_utc" type="DATETIME">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_on_utc" type="DATETIME">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
PostgreSQL – m_client_kyc
|
||||||
|
═══════════════════════════════════════════════════════════════════ -->
|
||||||
|
<changeSet author="fineract" id="1" context="postgresql">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_client_kyc"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_client_kyc">
|
||||||
|
<column autoIncrement="true" name="id" type="BIGINT">
|
||||||
|
<constraints nullable="false" primaryKey="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="client_id" type="BIGINT">
|
||||||
|
<constraints nullable="false" unique="true" uniqueConstraintName="uq_m_client_kyc_client_id"/>
|
||||||
|
</column>
|
||||||
|
<!-- INTEGER used (not SMALLINT) to match Java Integer JPA default mapping -->
|
||||||
|
<column name="kyc_status" type="INTEGER" defaultValueNumeric="100">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="kyc_type" type="INTEGER">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="risk_category" type="INTEGER" defaultValueNumeric="1">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="pan_number" type="VARCHAR(10)"/>
|
||||||
|
<column name="pan_verified" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="pan_verified_on" type="DATE"/>
|
||||||
|
<column name="aadhaar_number_masked" type="VARCHAR(12)"/>
|
||||||
|
<column name="aadhaar_verified" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="aadhaar_verified_on" type="DATE"/>
|
||||||
|
<column name="gstin_number" type="VARCHAR(15)"/>
|
||||||
|
<column name="gstin_verified" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="gstin_verified_on" type="DATE"/>
|
||||||
|
<column name="ckyc_number" type="VARCHAR(14)"/>
|
||||||
|
<column name="initiated_on" type="DATE"/>
|
||||||
|
<column name="verified_on" type="DATE"/>
|
||||||
|
<column name="rejected_on" type="DATE"/>
|
||||||
|
<column name="expiry_date" type="DATE"/>
|
||||||
|
<column name="remarks" type="VARCHAR(500)"/>
|
||||||
|
<column name="created_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
FK: m_client_kyc.client_id → m_client.id
|
||||||
|
═══════════════════════════════════════════════════════════════════ -->
|
||||||
|
<changeSet author="fineract" id="2">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not>
|
||||||
|
<foreignKeyConstraintExists foreignKeyTableName="m_client_kyc"
|
||||||
|
foreignKeyName="FK_m_client_kyc_client_id"/>
|
||||||
|
</not>
|
||||||
|
</preConditions>
|
||||||
|
<addForeignKeyConstraint
|
||||||
|
baseTableName="m_client_kyc"
|
||||||
|
baseColumnNames="client_id"
|
||||||
|
constraintName="FK_m_client_kyc_client_id"
|
||||||
|
referencedTableName="m_client"
|
||||||
|
referencedColumnNames="id"
|
||||||
|
onDelete="RESTRICT" onUpdate="RESTRICT"
|
||||||
|
deferrable="false" initiallyDeferred="false" validate="true"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
MySQL – m_client_kyc_verification_log (3rd-party API audit trail)
|
||||||
|
═══════════════════════════════════════════════════════════════════ -->
|
||||||
|
<changeSet author="fineract" id="3" context="mysql">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_client_kyc_verification_log"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_client_kyc_verification_log">
|
||||||
|
<column autoIncrement="true" name="id" type="BIGINT">
|
||||||
|
<constraints nullable="false" primaryKey="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="kyc_id" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<!-- verification_type: PAN | AADHAAR | GSTIN -->
|
||||||
|
<column name="verification_type" type="VARCHAR(20)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<!-- Status: SUCCESS | FAILURE | PENDING | ERROR -->
|
||||||
|
<column name="verification_status" type="VARCHAR(20)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<!-- External reference id returned by the 3rd-party API -->
|
||||||
|
<column name="request_reference" type="VARCHAR(100)"/>
|
||||||
|
<!-- Raw API response stored for audit -->
|
||||||
|
<column name="api_response" type="TEXT"/>
|
||||||
|
<column name="remarks" type="VARCHAR(500)"/>
|
||||||
|
<column name="created_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_on_utc" type="DATETIME">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_on_utc" type="DATETIME">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- PostgreSQL – m_client_kyc_verification_log -->
|
||||||
|
<changeSet author="fineract" id="3" context="postgresql">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_client_kyc_verification_log"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_client_kyc_verification_log">
|
||||||
|
<column autoIncrement="true" name="id" type="BIGINT">
|
||||||
|
<constraints nullable="false" primaryKey="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="kyc_id" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="verification_type" type="VARCHAR(20)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="verification_status" type="VARCHAR(20)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="request_reference" type="VARCHAR(100)"/>
|
||||||
|
<column name="api_response" type="TEXT"/>
|
||||||
|
<column name="remarks" type="VARCHAR(500)"/>
|
||||||
|
<column name="created_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- FK: m_client_kyc_verification_log.kyc_id → m_client_kyc.id -->
|
||||||
|
<changeSet author="fineract" id="4">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not>
|
||||||
|
<foreignKeyConstraintExists foreignKeyTableName="m_client_kyc_verification_log"
|
||||||
|
foreignKeyName="FK_m_kyc_verification_log_kyc_id"/>
|
||||||
|
</not>
|
||||||
|
</preConditions>
|
||||||
|
<addForeignKeyConstraint
|
||||||
|
baseTableName="m_client_kyc_verification_log"
|
||||||
|
baseColumnNames="kyc_id"
|
||||||
|
constraintName="FK_m_kyc_verification_log_kyc_id"
|
||||||
|
referencedTableName="m_client_kyc"
|
||||||
|
referencedColumnNames="id"
|
||||||
|
onDelete="CASCADE" onUpdate="RESTRICT"
|
||||||
|
deferrable="false" initiallyDeferred="false" validate="true"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
Permissions – inserted into m_permission
|
||||||
|
═══════════════════════════════════════════════════════════════════ -->
|
||||||
|
<changeSet author="fineract" id="5">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">
|
||||||
|
SELECT COUNT(*) FROM m_permission WHERE code = 'READ_CLIENTKYC'
|
||||||
|
</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="READ_CLIENTKYC"/>
|
||||||
|
<column name="entity_name" value="CLIENTKYC"/>
|
||||||
|
<column name="action_name" value="READ"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="CREATE_CLIENTKYC"/>
|
||||||
|
<column name="entity_name" value="CLIENTKYC"/>
|
||||||
|
<column name="action_name" value="CREATE"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="true"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="UPDATE_CLIENTKYC"/>
|
||||||
|
<column name="entity_name" value="CLIENTKYC"/>
|
||||||
|
<column name="action_name" value="UPDATE"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="true"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="VERIFY_CLIENTKYC"/>
|
||||||
|
<column name="entity_name" value="CLIENTKYC"/>
|
||||||
|
<column name="action_name" value="VERIFY"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="true"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="REJECT_CLIENTKYC"/>
|
||||||
|
<column name="entity_name" value="CLIENTKYC"/>
|
||||||
|
<column name="action_name" value="REJECT"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="true"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
|
||||||
|
|
||||||
|
<changeSet author="fineract" id="0217-1">
|
||||||
|
<addColumn tableName="m_family_members">
|
||||||
|
<column name="nomination_type" type="VARCHAR(50)" defaultValue="NULL">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="percentage_share" type="DECIMAL(5,2)" defaultValue="NULL">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="remarks" type="VARCHAR(500)" defaultValue="NULL">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
|
||||||
|
|
||||||
|
<changeSet author="fineract" id="0218-1" context="mysql">
|
||||||
|
<addColumn tableName="m_client_kyc">
|
||||||
|
<column name="pan_full_name" type="VARCHAR(200)" />
|
||||||
|
<column name="pan_aadhaar_seeding_status" type="VARCHAR(5)" remarks="Y=YES, N=NO, NA=Not Available" />
|
||||||
|
<column name="pan_category" type="VARCHAR(50)" remarks="PAN card category e.g. individual, company, firm" />
|
||||||
|
<column name="pan_status" type="VARCHAR(20)" remarks="valid or invalid" />
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="fineract" id="0218-1" context="postgresql">
|
||||||
|
<addColumn tableName="m_client_kyc">
|
||||||
|
<column name="pan_full_name" type="VARCHAR(200)" />
|
||||||
|
<column name="pan_aadhaar_seeding_status" type="VARCHAR(5)" remarks="Y=YES, N=NO, NA=Not Available" />
|
||||||
|
<column name="pan_category" type="VARCHAR(50)" remarks="PAN card category e.g. individual, company, firm" />
|
||||||
|
<column name="pan_status" type="VARCHAR(20)" remarks="valid or invalid" />
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||||
|
|
||||||
|
<changeSet author="fineract" id="rbi_feed-1">
|
||||||
|
<createTable tableName="m_rbi_feed_item">
|
||||||
|
<column autoIncrement="true" name="id" type="BIGINT">
|
||||||
|
<constraints nullable="false" primaryKey="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="title" type="VARCHAR(1000)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="description" type="TEXT"/>
|
||||||
|
<column name="link" type="VARCHAR(1000)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="pub_date" type="DATETIME"/>
|
||||||
|
<column name="category" type="VARCHAR(50)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="fetched_on" type="DATETIME"/>
|
||||||
|
</createTable>
|
||||||
|
<addUniqueConstraint tableName="m_rbi_feed_item" columnNames="link" constraintName="uk_rbi_feed_item_link"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="fineract" id="rbi_feed-2">
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="organisation"/>
|
||||||
|
<column name="code" value="READ_RBI_FEED"/>
|
||||||
|
<column name="entity_name" value="RBI_FEED"/>
|
||||||
|
<column name="action_name" value="READ"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="organisation"/>
|
||||||
|
<column name="code" value="FETCH_RBI_FEED"/>
|
||||||
|
<column name="entity_name" value="RBI_FEED"/>
|
||||||
|
<column name="action_name" value="FETCH"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="fineract" id="rbi_feed-3">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<insert tableName="job">
|
||||||
|
<column name="name" value="Fetch RBI RSS Feeds"/>
|
||||||
|
<column name="display_name" value="Fetch RBI RSS Feeds"/>
|
||||||
|
<column name="short_name" value="RBIRSS"/>
|
||||||
|
<column name="cron_expression" value="0 0 */6 * * ?"/>
|
||||||
|
<column name="create_time" valueDate="${current_datetime}"/>
|
||||||
|
<column name="task_priority" valueNumeric="5"/>
|
||||||
|
<column name="group_name"/>
|
||||||
|
<column name="previous_run_start_time"/>
|
||||||
|
<column name="job_key" value="Fetch RBI RSS FeedsDetail1 _ DEFAULT"/>
|
||||||
|
<column name="initializing_errorlog"/>
|
||||||
|
<column name="is_active" valueBoolean="true"/>
|
||||||
|
<column name="currently_running" valueBoolean="false"/>
|
||||||
|
<column name="updates_allowed" valueBoolean="true"/>
|
||||||
|
<column name="scheduler_group" valueNumeric="0"/>
|
||||||
|
<column name="is_misfired" valueBoolean="false"/>
|
||||||
|
<column name="node_id" valueNumeric="1"/>
|
||||||
|
<column name="is_mismatched_job" valueBoolean="true"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet author="fineract" id="0220-1" context="default">
|
||||||
|
<comment>Add OpenProject read permission</comment>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="infrastructure"/>
|
||||||
|
<column name="code" value="READ_OPENPROJECT"/>
|
||||||
|
<column name="entity_name" value="OPENPROJECT"/>
|
||||||
|
<column name="action_name" value="READ"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="fineract" id="0220-2" context="default">
|
||||||
|
<comment>Add OpenProject create/write permission</comment>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="infrastructure"/>
|
||||||
|
<column name="code" value="CREATE_OPENPROJECT"/>
|
||||||
|
<column name="entity_name" value="OPENPROJECT"/>
|
||||||
|
<column name="action_name" value="CREATE"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet author="fineract" id="0221-1" context="default">
|
||||||
|
<comment>Add OpenProject work package ID and status columns to RBI feed items for tracking</comment>
|
||||||
|
<addColumn tableName="m_rbi_feed_item">
|
||||||
|
<column name="openproject_wp_id" type="BIGINT" defaultValue="null">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="openproject_wp_status" type="VARCHAR(100)" defaultValue="null">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="openproject_wp_progress" type="INT" defaultValue="null">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+276
@@ -0,0 +1,276 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<!--
|
||||||
|
Sanctions screening module – stores UN Security Council consolidated sanctions list.
|
||||||
|
|
||||||
|
Tables created:
|
||||||
|
m_sanctions_entry – individuals and entities from the UN consolidated list
|
||||||
|
m_sanctions_alias – aliases (aka) for each sanctions entry
|
||||||
|
|
||||||
|
Permissions inserted:
|
||||||
|
READ_SANCTIONS, SCREEN_SANCTIONS, LOAD_SANCTIONS
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
MySQL – m_sanctions_entry
|
||||||
|
═══════════════════════════════════════════════════════════════════ -->
|
||||||
|
<changeSet author="fineract" id="1" context="mysql">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_sanctions_entry"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_sanctions_entry">
|
||||||
|
<column autoIncrement="true" name="id" type="BIGINT">
|
||||||
|
<constraints nullable="false" primaryKey="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="data_id" type="VARCHAR(50)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<!-- entry_type: INDIVIDUAL or ENTITY -->
|
||||||
|
<column name="entry_type" type="VARCHAR(20)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="first_name" type="VARCHAR(255)"/>
|
||||||
|
<column name="second_name" type="VARCHAR(255)"/>
|
||||||
|
<column name="third_name" type="VARCHAR(255)"/>
|
||||||
|
<column name="fourth_name" type="VARCHAR(255)"/>
|
||||||
|
<!-- full_name: concatenation of all name parts for easy searching -->
|
||||||
|
<column name="full_name" type="VARCHAR(500)"/>
|
||||||
|
<column name="un_list_type" type="VARCHAR(255)"/>
|
||||||
|
<column name="reference_number" type="VARCHAR(255)"/>
|
||||||
|
<column name="listed_on" type="DATE"/>
|
||||||
|
<column name="gender" type="VARCHAR(20)"/>
|
||||||
|
<column name="nationality" type="VARCHAR(500)"/>
|
||||||
|
<column name="comments" type="TEXT"/>
|
||||||
|
<column name="designation" type="TEXT"/>
|
||||||
|
<column name="date_of_birth" type="VARCHAR(500)"/>
|
||||||
|
<column name="place_of_birth" type="VARCHAR(500)"/>
|
||||||
|
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<!-- Audit fields -->
|
||||||
|
<column name="created_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_on_utc" type="DATETIME">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_on_utc" type="DATETIME">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
PostgreSQL – m_sanctions_entry
|
||||||
|
═══════════════════════════════════════════════════════════════════ -->
|
||||||
|
<changeSet author="fineract" id="1" context="postgresql">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_sanctions_entry"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_sanctions_entry">
|
||||||
|
<column autoIncrement="true" name="id" type="BIGINT">
|
||||||
|
<constraints nullable="false" primaryKey="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="data_id" type="VARCHAR(50)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="entry_type" type="VARCHAR(20)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="first_name" type="VARCHAR(255)"/>
|
||||||
|
<column name="second_name" type="VARCHAR(255)"/>
|
||||||
|
<column name="third_name" type="VARCHAR(255)"/>
|
||||||
|
<column name="fourth_name" type="VARCHAR(255)"/>
|
||||||
|
<column name="full_name" type="VARCHAR(500)"/>
|
||||||
|
<column name="un_list_type" type="VARCHAR(255)"/>
|
||||||
|
<column name="reference_number" type="VARCHAR(255)"/>
|
||||||
|
<column name="listed_on" type="DATE"/>
|
||||||
|
<column name="gender" type="VARCHAR(20)"/>
|
||||||
|
<column name="nationality" type="VARCHAR(500)"/>
|
||||||
|
<column name="comments" type="TEXT"/>
|
||||||
|
<column name="designation" type="TEXT"/>
|
||||||
|
<column name="date_of_birth" type="VARCHAR(500)"/>
|
||||||
|
<column name="place_of_birth" type="VARCHAR(500)"/>
|
||||||
|
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
MySQL – m_sanctions_alias (aka names for sanctions entries)
|
||||||
|
═══════════════════════════════════════════════════════════════════ -->
|
||||||
|
<changeSet author="fineract" id="2" context="mysql">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_sanctions_alias"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_sanctions_alias">
|
||||||
|
<column autoIncrement="true" name="id" type="BIGINT">
|
||||||
|
<constraints nullable="false" primaryKey="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="sanctions_entry_id" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="alias_quality" type="VARCHAR(50)"/>
|
||||||
|
<column name="alias_name" type="VARCHAR(500)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_on_utc" type="DATETIME">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_on_utc" type="DATETIME">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- PostgreSQL – m_sanctions_alias -->
|
||||||
|
<changeSet author="fineract" id="2" context="postgresql">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_sanctions_alias"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_sanctions_alias">
|
||||||
|
<column autoIncrement="true" name="id" type="BIGINT">
|
||||||
|
<constraints nullable="false" primaryKey="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="sanctions_entry_id" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="alias_quality" type="VARCHAR(50)"/>
|
||||||
|
<column name="alias_name" type="VARCHAR(500)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- FK: m_sanctions_alias.sanctions_entry_id → m_sanctions_entry.id -->
|
||||||
|
<changeSet author="fineract" id="3">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not>
|
||||||
|
<foreignKeyConstraintExists foreignKeyTableName="m_sanctions_alias"
|
||||||
|
foreignKeyName="FK_m_sanctions_alias_entry_id"/>
|
||||||
|
</not>
|
||||||
|
</preConditions>
|
||||||
|
<addForeignKeyConstraint
|
||||||
|
baseTableName="m_sanctions_alias"
|
||||||
|
baseColumnNames="sanctions_entry_id"
|
||||||
|
constraintName="FK_m_sanctions_alias_entry_id"
|
||||||
|
referencedTableName="m_sanctions_entry"
|
||||||
|
referencedColumnNames="id"
|
||||||
|
onDelete="CASCADE" onUpdate="RESTRICT"
|
||||||
|
deferrable="false" initiallyDeferred="false" validate="true"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Index on full_name for faster name searches -->
|
||||||
|
<changeSet author="fineract" id="4">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not>
|
||||||
|
<indexExists indexName="idx_sanctions_entry_full_name"/>
|
||||||
|
</not>
|
||||||
|
</preConditions>
|
||||||
|
<createIndex tableName="m_sanctions_entry" indexName="idx_sanctions_entry_full_name">
|
||||||
|
<column name="full_name"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Index on alias_name for faster alias searches -->
|
||||||
|
<changeSet author="fineract" id="5">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not>
|
||||||
|
<indexExists indexName="idx_sanctions_alias_name"/>
|
||||||
|
</not>
|
||||||
|
</preConditions>
|
||||||
|
<createIndex tableName="m_sanctions_alias" indexName="idx_sanctions_alias_name">
|
||||||
|
<column name="alias_name"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
Permissions
|
||||||
|
═══════════════════════════════════════════════════════════════════ -->
|
||||||
|
<changeSet author="fineract" id="6">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">
|
||||||
|
SELECT COUNT(*) FROM m_permission WHERE code = 'READ_SANCTIONS'
|
||||||
|
</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="READ_SANCTIONS"/>
|
||||||
|
<column name="entity_name" value="SANCTIONS"/>
|
||||||
|
<column name="action_name" value="READ"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="SCREEN_SANCTIONS"/>
|
||||||
|
<column name="entity_name" value="SANCTIONS"/>
|
||||||
|
<column name="action_name" value="SCREEN"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="LOAD_SANCTIONS"/>
|
||||||
|
<column name="entity_name" value="SANCTIONS"/>
|
||||||
|
<column name="action_name" value="LOAD"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="true"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<!--
|
||||||
|
Sanctions screening activity log – records every name screening request and result.
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<!-- MySQL – m_sanctions_screening_log -->
|
||||||
|
<changeSet author="fineract" id="1" context="mysql">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_sanctions_screening_log"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_sanctions_screening_log">
|
||||||
|
<column autoIncrement="true" name="id" type="BIGINT">
|
||||||
|
<constraints nullable="false" primaryKey="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="screened_name" type="VARCHAR(500)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<!-- FLAGGED or CLEAR -->
|
||||||
|
<column name="screening_result" type="VARCHAR(20)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="match_count" type="INTEGER" defaultValueNumeric="0">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<!-- Comma-separated matched entry IDs for reference -->
|
||||||
|
<column name="matched_entry_ids" type="VARCHAR(1000)"/>
|
||||||
|
<!-- Comma-separated matched names for quick reference -->
|
||||||
|
<column name="matched_names" type="VARCHAR(2000)"/>
|
||||||
|
<!-- CLIENT_CREATION, MANUAL_CHECK, etc. -->
|
||||||
|
<column name="screening_source" type="VARCHAR(50)"/>
|
||||||
|
<!-- Optional: client ID if screening during client creation -->
|
||||||
|
<column name="client_id" type="BIGINT"/>
|
||||||
|
<column name="ip_address" type="VARCHAR(50)"/>
|
||||||
|
<column name="screened_on" type="DATETIME">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_on_utc" type="DATETIME">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_on_utc" type="DATETIME">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- PostgreSQL – m_sanctions_screening_log -->
|
||||||
|
<changeSet author="fineract" id="1" context="postgresql">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_sanctions_screening_log"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_sanctions_screening_log">
|
||||||
|
<column autoIncrement="true" name="id" type="BIGINT">
|
||||||
|
<constraints nullable="false" primaryKey="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="screened_name" type="VARCHAR(500)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="screening_result" type="VARCHAR(20)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="match_count" type="INTEGER" defaultValueNumeric="0">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="matched_entry_ids" type="VARCHAR(1000)"/>
|
||||||
|
<column name="matched_names" type="VARCHAR(2000)"/>
|
||||||
|
<column name="screening_source" type="VARCHAR(50)"/>
|
||||||
|
<column name="client_id" type="BIGINT"/>
|
||||||
|
<column name="ip_address" type="VARCHAR(50)"/>
|
||||||
|
<column name="screened_on" type="TIMESTAMP WITH TIME ZONE">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Index on screened_name for searching logs -->
|
||||||
|
<changeSet author="fineract" id="2">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><indexExists indexName="idx_sanctions_log_screened_name"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createIndex tableName="m_sanctions_screening_log" indexName="idx_sanctions_log_screened_name">
|
||||||
|
<column name="screened_name"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Index on screened_on for date range queries -->
|
||||||
|
<changeSet author="fineract" id="3">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><indexExists indexName="idx_sanctions_log_screened_on"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createIndex tableName="m_sanctions_screening_log" indexName="idx_sanctions_log_screened_on">
|
||||||
|
<column name="screened_on"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<!--
|
||||||
|
Mobile Number Lookup – activity log for DIP / MNRL lookups.
|
||||||
|
|
||||||
|
Table created:
|
||||||
|
m_mobile_lookup_log – logs every mobile number lookup request and response
|
||||||
|
|
||||||
|
Permissions inserted:
|
||||||
|
READ_MOBILE_LOOKUP
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
MySQL – m_mobile_lookup_log
|
||||||
|
═══════════════════════════════════════════════════════════════════ -->
|
||||||
|
<changeSet author="fineract" id="1" context="mysql">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_mobile_lookup_log"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_mobile_lookup_log">
|
||||||
|
<column autoIncrement="true" name="id" type="BIGINT">
|
||||||
|
<constraints nullable="false" primaryKey="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="mobile_number" type="VARCHAR(15)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<!-- CLEAN, SUSPECTED, DISCONNECTED, SUSPECTED_AND_DISCONNECTED, ERROR -->
|
||||||
|
<column name="lookup_status" type="VARCHAR(50)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="suspected" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="disconnected" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="response_message" type="VARCHAR(500)"/>
|
||||||
|
<column name="raw_response" type="TEXT"/>
|
||||||
|
<!-- CLIENT_REGISTRATION, MANUAL_CHECK, API -->
|
||||||
|
<column name="lookup_source" type="VARCHAR(50)"/>
|
||||||
|
<column name="client_id" type="BIGINT"/>
|
||||||
|
<column name="looked_up_on" type="DATETIME">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<!-- Audit fields -->
|
||||||
|
<column name="created_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_on_utc" type="DATETIME">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_on_utc" type="DATETIME">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- PostgreSQL – m_mobile_lookup_log -->
|
||||||
|
<changeSet author="fineract" id="1" context="postgresql">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_mobile_lookup_log"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_mobile_lookup_log">
|
||||||
|
<column autoIncrement="true" name="id" type="BIGINT">
|
||||||
|
<constraints nullable="false" primaryKey="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="mobile_number" type="VARCHAR(15)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="lookup_status" type="VARCHAR(50)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="suspected" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="disconnected" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="response_message" type="VARCHAR(500)"/>
|
||||||
|
<column name="raw_response" type="TEXT"/>
|
||||||
|
<column name="lookup_source" type="VARCHAR(50)"/>
|
||||||
|
<column name="client_id" type="BIGINT"/>
|
||||||
|
<column name="looked_up_on" type="TIMESTAMP WITH TIME ZONE">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_by" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Index on mobile_number for faster lookups -->
|
||||||
|
<changeSet author="fineract" id="2">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not>
|
||||||
|
<indexExists indexName="idx_mobile_lookup_log_number"/>
|
||||||
|
</not>
|
||||||
|
</preConditions>
|
||||||
|
<createIndex tableName="m_mobile_lookup_log" indexName="idx_mobile_lookup_log_number">
|
||||||
|
<column name="mobile_number"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Index on looked_up_on for recent-first queries -->
|
||||||
|
<changeSet author="fineract" id="3">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not>
|
||||||
|
<indexExists indexName="idx_mobile_lookup_log_looked_up_on"/>
|
||||||
|
</not>
|
||||||
|
</preConditions>
|
||||||
|
<createIndex tableName="m_mobile_lookup_log" indexName="idx_mobile_lookup_log_looked_up_on">
|
||||||
|
<column name="looked_up_on"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
Permissions
|
||||||
|
═══════════════════════════════════════════════════════════════════ -->
|
||||||
|
<changeSet author="fineract" id="4">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">
|
||||||
|
SELECT COUNT(*) FROM m_permission WHERE code = 'READ_MOBILE_LOOKUP'
|
||||||
|
</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="portfolio"/>
|
||||||
|
<column name="code" value="READ_MOBILE_LOOKUP"/>
|
||||||
|
<column name="entity_name" value="MOBILE_LOOKUP"/>
|
||||||
|
<column name="action_name" value="READ"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -35,10 +35,25 @@
|
|||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
<!-- File appender — logs saved to logs/fineract.log, rotated daily, max 30 days -->
|
||||||
|
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>logs/fineract.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>logs/fineract.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} : %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
<!-- But these three INFO are still handy ;-) just to see when it's up and running -->
|
<!-- But these three INFO are still handy ;-) just to see when it's up and running -->
|
||||||
<logger name="org.springframework.boot.web.embedded.tomcat.TomcatWebServer" level="info"/>
|
<logger name="org.springframework.boot.web.embedded.tomcat.TomcatWebServer" level="info"/>
|
||||||
<logger name="org.apache.fineract.ServerApplication" level="info"/>
|
<logger name="org.apache.fineract.ServerApplication" level="info"/>
|
||||||
<logger name="org.apache.fineract" level="${FINERACT_LOGGING_LEVEL:-INFO}"/>
|
<logger name="org.apache.fineract" level="${FINERACT_LOGGING_LEVEL:-INFO}"/>
|
||||||
|
<!-- DEBUG for RBI Feed and OpenProject modules -->
|
||||||
|
<logger name="org.apache.fineract.infrastructure.rbifeed" level="DEBUG"/>
|
||||||
|
<logger name="org.apache.fineract.infrastructure.openproject" level="DEBUG"/>
|
||||||
<logger name="liquibase" level="warn"/>
|
<logger name="liquibase" level="warn"/>
|
||||||
|
|
||||||
<if condition="${jsonLoggingEnabled} == true">
|
<if condition="${jsonLoggingEnabled} == true">
|
||||||
@@ -61,6 +76,7 @@
|
|||||||
but it's still possible to override this using java -Dlogging.level.root=info -jar fineract-provider.jar, as per https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-custom-log-levels -->
|
but it's still possible to override this using java -Dlogging.level.root=info -jar fineract-provider.jar, as per https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-custom-log-levels -->
|
||||||
<root level="info">
|
<root level="info">
|
||||||
<appender-ref ref="CONSOLE"/>
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
<appender-ref ref="FILE"/>
|
||||||
</root>
|
</root>
|
||||||
</else>
|
</else>
|
||||||
</if>
|
</if>
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- KYC Module – Table Creation Script (MySQL / MariaDB)
|
||||||
|
-- =============================================================================
|
||||||
|
-- DB Connection (from config/docker/env/fineract-common.env & mariadb.env):
|
||||||
|
-- Host : localhost (or 'db' inside Docker)
|
||||||
|
-- Port : 3306
|
||||||
|
-- Database : fineract_default ← tenant database (your actual data lives here)
|
||||||
|
-- Username : root
|
||||||
|
-- Password : skdcnwauicn2ucnaecasdsajdnizucawencascdca
|
||||||
|
--
|
||||||
|
-- Run command:
|
||||||
|
-- mysql -h localhost -P 3306 -u root -p'skdcnwauicn2ucnaecasdsajdnizucawencascdca' fineract_default < create_kyc_tables_mysql_mariadb.sql
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
USE fineract_default;
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- TABLE: m_client_kyc
|
||||||
|
-- One KYC record per client (enforced by UNIQUE on client_id).
|
||||||
|
-- Stores PAN, Aadhaar (masked), GSTIN, CKYCRR number, risk category,
|
||||||
|
-- lifecycle dates and audit fields.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `m_client_kyc` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
`client_id` BIGINT NOT NULL,
|
||||||
|
|
||||||
|
-- kyc_status : 100=PENDING | 200=IN_PROGRESS | 300=VERIFIED | 400=REJECTED | 500=EXPIRED
|
||||||
|
`kyc_status` SMALLINT NOT NULL DEFAULT 100,
|
||||||
|
|
||||||
|
-- kyc_type : 1=INDIVIDUAL | 2=NON_INDIVIDUAL
|
||||||
|
`kyc_type` SMALLINT NOT NULL,
|
||||||
|
|
||||||
|
-- risk_category : 1=LOW (re-KYC 10yr) | 2=MEDIUM (8yr) | 3=HIGH (2yr)
|
||||||
|
`risk_category` SMALLINT NOT NULL DEFAULT 1,
|
||||||
|
|
||||||
|
-- PAN (Permanent Account Number – Income Tax Dept)
|
||||||
|
-- Format: 5 uppercase letters + 4 digits + 1 uppercase letter e.g. ABCDE1234F
|
||||||
|
`pan_number` VARCHAR(10) NULL,
|
||||||
|
`pan_verified` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`pan_verified_on` DATE NULL,
|
||||||
|
|
||||||
|
-- Aadhaar – ONLY masked form stored (last 4 digits visible) per UIDAI directive
|
||||||
|
-- e.g. XXXXXXXX1234
|
||||||
|
`aadhaar_number_masked` VARCHAR(12) NULL,
|
||||||
|
`aadhaar_verified` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`aadhaar_verified_on` DATE NULL,
|
||||||
|
|
||||||
|
-- GSTIN (15-char GST Identification Number – mandatory for NON_INDIVIDUAL)
|
||||||
|
-- Format: 2-digit state code + 10-char PAN + 1 entity num + Z + check digit
|
||||||
|
`gstin_number` VARCHAR(15) NULL,
|
||||||
|
`gstin_verified` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`gstin_verified_on` DATE NULL,
|
||||||
|
|
||||||
|
-- CKYCRR – Central KYC Records Registry number (issued by CERSAI, 14 digits)
|
||||||
|
`ckyc_number` VARCHAR(14) NULL,
|
||||||
|
|
||||||
|
-- Lifecycle timestamps
|
||||||
|
`initiated_on` DATE NULL,
|
||||||
|
`verified_on` DATE NULL,
|
||||||
|
`rejected_on` DATE NULL,
|
||||||
|
-- expiry_date computed as: verified_on + risk_category years offset
|
||||||
|
`expiry_date` DATE NULL,
|
||||||
|
`remarks` VARCHAR(500) NULL,
|
||||||
|
|
||||||
|
-- Audit fields (AbstractAuditableWithUTCDateTimeCustom)
|
||||||
|
`created_by` BIGINT NOT NULL,
|
||||||
|
`created_on_utc` DATETIME NOT NULL,
|
||||||
|
`last_modified_by` BIGINT NOT NULL,
|
||||||
|
`last_modified_on_utc` DATETIME NOT NULL,
|
||||||
|
`version` BIGINT NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_m_client_kyc_client_id` (`client_id`),
|
||||||
|
CONSTRAINT `FK_m_client_kyc_client_id`
|
||||||
|
FOREIGN KEY (`client_id`) REFERENCES `m_client` (`id`)
|
||||||
|
ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- TABLE: m_client_kyc_verification_log
|
||||||
|
-- Immutable audit trail for every 3rd-party PAN / Aadhaar / GSTIN API call.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `m_client_kyc_verification_log` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
`kyc_id` BIGINT NOT NULL,
|
||||||
|
|
||||||
|
-- verification_type: PAN | AADHAAR | GSTIN
|
||||||
|
`verification_type` VARCHAR(20) NOT NULL,
|
||||||
|
|
||||||
|
-- verification_status: SUCCESS | FAILURE | PENDING | ERROR
|
||||||
|
`verification_status` VARCHAR(20) NOT NULL,
|
||||||
|
|
||||||
|
-- External reference id returned by the 3rd-party vendor API
|
||||||
|
`request_reference` VARCHAR(100) NULL,
|
||||||
|
|
||||||
|
-- Full raw JSON response from the vendor – stored for compliance audit
|
||||||
|
`api_response` TEXT NULL,
|
||||||
|
`remarks` VARCHAR(500) NULL,
|
||||||
|
|
||||||
|
-- Audit fields
|
||||||
|
`created_by` BIGINT NOT NULL,
|
||||||
|
`created_on_utc` DATETIME NOT NULL,
|
||||||
|
`last_modified_by` BIGINT NOT NULL,
|
||||||
|
`last_modified_on_utc` DATETIME NOT NULL,
|
||||||
|
`version` BIGINT NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
CONSTRAINT `FK_m_kyc_verification_log_kyc_id`
|
||||||
|
FOREIGN KEY (`kyc_id`) REFERENCES `m_client_kyc` (`id`)
|
||||||
|
ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- PERMISSIONS – Insert into m_permission
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `m_permission`
|
||||||
|
(`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`)
|
||||||
|
VALUES
|
||||||
|
('portfolio', 'READ_CLIENTKYC', 'CLIENTKYC', 'READ', 0),
|
||||||
|
('portfolio', 'CREATE_CLIENTKYC', 'CLIENTKYC', 'CREATE', 1),
|
||||||
|
('portfolio', 'UPDATE_CLIENTKYC', 'CLIENTKYC', 'UPDATE', 1),
|
||||||
|
('portfolio', 'VERIFY_CLIENTKYC', 'CLIENTKYC', 'VERIFY', 1),
|
||||||
|
('portfolio', 'REJECT_CLIENTKYC', 'CLIENTKYC', 'REJECT', 1);
|
||||||
|
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- VERIFY CREATION
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
SELECT 'Tables created:' AS info;
|
||||||
|
SHOW TABLES LIKE 'm_client_kyc%';
|
||||||
|
|
||||||
|
SELECT 'Permissions inserted:' AS info;
|
||||||
|
SELECT code, entity_name, action_name FROM m_permission WHERE entity_name = 'CLIENTKYC';
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user