Add extended fields to ClientNonPersonDetails: authorizedSignatory, additionalInfo, dateOfRegistration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-04-04 15:06:45 +05:30
co-authored by Claude Opus 4.6
parent 90c21d40c1
commit 87e132f93d
147 changed files with 422232 additions and 167 deletions
+30
View File
@@ -0,0 +1,30 @@
{
"permissions": {
"allow": [
"Bash(git remote add self-hosted https://git.zeptic.in/CBSNexora/JAVA-Backend-CBS-MAIN.git)",
"Bash(git push self-hosted develop:main)",
"WebFetch(domain:rbi.org.in)",
"Bash(xargs grep -l \"@Scheduled\")",
"Bash(./gradlew compileJava -x spotlessCheck)",
"Bash(./gradlew bootJar -x test -x spotlessCheck)",
"Bash(./gradlew :fineract-provider:compileJava -x spotlessCheck)",
"Bash(./gradlew :fineract-core:compileJava :fineract-provider:compileJava)",
"Bash(curl -sk -o /dev/null -w \"%{http_code}\" https://localhost:8443/fineract-provider/api/v1/rbi-feeds)",
"Bash(./gradlew bootRun -x test)",
"Bash(ls *.gradle)",
"Bash(curl -sk -o /dev/null -w \"%{http_code}\" https://localhost:8443/fineract-provider/api)",
"Bash(ls c:/Users/adity/Music/CBS/fineract/fineract-provider/src/main/resources/db/changelog/tenant/parts/0232*)",
"Bash(taskkill //F //PID 36040)",
"Bash(findstr :8443)",
"Bash(findstr LISTENING)",
"Bash(taskkill //F //PID 14668)",
"Bash(./gradlew --stop)"
],
"additionalDirectories": [
"C:\\Users\\adity\\Music\\CBS\\UI\\web-app\\src\\app\\rbi-circulars",
"C:\\Users\\adity\\Music\\CBS\\UI\\web-app\\src\\app\\clients\\create-client",
"C:\\Users\\adity\\Music\\CBS\\UI\\web-app\\src\\app\\clients\\client-stepper\\client-general-step",
"C:\\Users\\adity\\Music\\CBS\\UI\\web-app\\src\\app"
]
}
}
+45
View File
@@ -0,0 +1,45 @@
{
"permissions": {
"allow": [
"Bash(taskkill //PID 24168 //F)",
"Bash(PGPASSWORD=postgres psql -h localhost -U postgres -d fineract_default -c \"SELECT id, display_name, date_of_birth FROM m_client ORDER BY id DESC LIMIT 5;\")",
"Bash(taskkill //PID 10112 //F)",
"Bash(taskkill //PID 39420 //F)",
"Bash(curl -sk -u \"mifos:password\" -H \"fineract-platform-tenantid: default\" \"https://localhost:8443/fineract-provider/api/v1/rbi-feeds\")",
"Bash(sort -t: -k2 -n)",
"Bash(curl -sk -u \"mifos:password\" -H \"fineract-platform-tenantid: default\" \"https://localhost:8443/fineract-provider/api/v1/rbi-feeds/144\")",
"Bash(curl -sk -u \"mifos:password\" -H \"fineract-platform-tenantid: default\" \"https://localhost:8443/fineract-provider/api/v1/openproject/work-packages?pageSize=10&offset=1\")",
"Bash(curl -sk -u \"mifos:password\" -H \"fineract-platform-tenantid: default\" -H \"Content-Type: application/json\" -X POST \"https://localhost:8443/fineract-provider/api/v1/rbi-feeds/sync-openproject\" -d '{}')",
"Bash(taskkill //PID 36832 //F)",
"Bash(mvn clean:*)",
"Bash(where mvn.cmd)",
"Bash(where mvn:*)",
"Read(//c/Program Files/**)",
"Read(//c/Users/adity/**)",
"Bash(echo \"$MAVEN_HOME\")",
"Bash(xargs basename:*)",
"Bash(./gradlew :fineract-tax:compileJava)",
"Bash(grep -n \"INSERT INTO \\\\`m_role\\\\`\\\\|INSERT INTO m_role\\\\|INSERT INTO \\\\`m_permission\\\\`\\\\|INSERT INTO m_permission\" /c/Users/adity/Music/CBS/fineract/fineract-provider/src/main/resources/sql/migrations/sample_data/load_sample_data.sql)",
"Bash(npm start:*)",
"Bash(grep -oP \"^\\\\\\\\\\(''''\\([^'''']+\\)''''\" \"c:\\\\Users\\\\adity\\\\Music\\\\CBS\\\\fineract\\\\fineract-db\\\\old-schema-files\\\\0003-mifosx-permissions-and-authorisation-utf8.sql\")",
"Bash(tasklist)",
"Bash(wmic process:*)",
"Bash(powershell -Command \"Get-CimInstance Win32_Process -Filter \"\"Name=''java.exe''\"\" | Select-Object ProcessId, @{N=''Cmd'';E={$_.CommandLine.Substring\\(0, [Math]::Min\\(200, $_.CommandLine.Length\\)\\)}} | Format-Table -AutoSize\")",
"Bash(psql -U postgres -d fineract_default -c \"UPDATE databasechangelog SET md5sum = NULL WHERE id = ''0231-1'' AND filename = ''db/changelog/tenant/parts/0231_add_menu_permissions.xml'';\" -c \"UPDATE m_permission SET grouping = ''menu'' WHERE code LIKE ''MENU_%'' AND entity_name = ''MENU'';\")",
"Bash(PGPASSWORD=root psql -U postgres -d fineract_default -c \"UPDATE databasechangelog SET md5sum = NULL WHERE id = ''0231-1'' AND filename = ''db/changelog/tenant/parts/0231_add_menu_permissions.xml'';\" -c \"UPDATE m_permission SET grouping = ''menu'' WHERE code LIKE ''MENU_%'' AND entity_name = ''MENU'';\")",
"Bash(PGPASSWORD=root psql -U postgres -d fineract_default -c \"UPDATE databasechangelog SET md5sum = NULL WHERE id = ''0231-1'' AND filename = ''db/changelog/tenant/parts/0231_add_menu_permissions.xml'';\" -c \"UPDATE m_permission SET grouping = ''heading_menu'' WHERE code LIKE ''MENU_%'' AND entity_name = ''MENU'';\")",
"Bash(grep -l \"pan_full_name\\\\|pan_category\\\\|pan_status\\\\|pan_aadhaar\" \"c:\\\\Users\\\\adity\\\\Music\\\\CBS\\\\fineract\\\\fineract-provider\\\\src\\\\main\\\\resources\\\\db\\\\changelog\\\\tenant\\\\parts\"/*.xml)",
"Bash(grep -l \"m_client_kyc\" \"c:\\\\Users\\\\adity\\\\Music\\\\CBS\\\\fineract\\\\fineract-provider\\\\src\\\\main\\\\resources\\\\db\\\\changelog\\\\tenant\\\\parts\"/*.xml)",
"Bash(./gradlew devRun -x test -x spotlessCheck)",
"Bash(taskkill //F //FI \"IMAGENAME eq java.exe\")",
"Bash(./gradlew bootRun)",
"Bash(curl -sk -X POST \"https://localhost:8443/fineract-provider/api/v1/authentication?username=mifos&password=password\" -H 'Fineract-Platform-TenantId: default' -H 'Content-Type: application/json')",
"Bash(curl -sk \"https://localhost:8443/fineract-provider/api/v1/clients?limit=5\" -H \"Fineract-Platform-TenantId: default\" -u \"mifos:password\")",
"Bash(curl -sk -X POST \"https://localhost:8443/fineract-provider/api/v1/authentication\" -H \"Fineract-Platform-TenantId: default\" -H \"Content-Type: application/json\" -d '{\"\"username\"\":\"\"mifos\"\",\"\"password\"\":\"\"password\"\"}')",
"Bash(curl -sk \"https://localhost:8443/fineract-provider/api/v1/clients?limit=5\" -H \"Fineract-Platform-TenantId: default\" -H \"Authorization: Basic bWlmb3M6cGFzc3dvcmQ=\")"
],
"additionalDirectories": [
"c:\\Users\\adity\\Music\\CBS\\cfcfrms-bank-api"
]
}
}
@@ -0,0 +1,365 @@
{
"info": {
"name": "CBS — CIN & DIN Verification (LincPay eKYC)",
"description": "Postman collection for CIN (Company Identification Number) and DIN (Director Identification Number) verification via the CBS Fineract KYC module.\n\nFlow:\n1. Create/update a KYC record with cinNumber and/or dinNumber\n2. Trigger verify-cin or verify-din command\n3. The backend calls LincPay eKYC API (validate-company / validate-director)\n4. On success, cinVerified/dinVerified flags are set on the KYC record",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{
"key": "base_url",
"value": "https://localhost:8443/fineract-provider/api/v1",
"description": "Fineract API base URL"
},
{
"key": "tenant_id",
"value": "default",
"description": "Tenant identifier"
},
{
"key": "username",
"value": "mifos",
"description": "API username"
},
{
"key": "password",
"value": "password",
"description": "API password"
},
{
"key": "client_id",
"value": "1",
"description": "Client ID for KYC operations"
},
{
"key": "kyc_id",
"value": "1",
"description": "KYC record ID"
},
{
"key": "cin_number",
"value": "U12345MH2020PTC123456",
"description": "21-character CIN for company verification"
},
{
"key": "din_number",
"value": "12345678",
"description": "8-digit DIN for director verification"
}
],
"auth": {
"type": "basic",
"basic": [
{ "key": "username", "value": "{{username}}", "type": "string" },
{ "key": "password", "value": "{{password}}", "type": "string" }
]
},
"item": [
{
"name": "1. Initiate KYC with CIN & DIN",
"item": [
{
"name": "Initiate KYC (Non-Individual with CIN & DIN)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test('KYC record created', function () {",
" var json = pm.response.json();",
" pm.expect(json).to.have.property('resourceId');",
" pm.collectionVariables.set('kyc_id', json.resourceId);",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
],
"body": {
"mode": "raw",
"raw": "{\n \"kycType\": 2,\n \"riskCategory\": 2,\n \"panNumber\": \"ABCDE1234F\",\n \"gstinNumber\": \"27ABCDE1234F1Z5\",\n \"cinNumber\": \"{{cin_number}}\",\n \"dinNumber\": \"{{din_number}}\",\n \"remarks\": \"Non-individual KYC with CIN and DIN\"\n}"
},
"url": {
"raw": "{{base_url}}/clients/{{client_id}}/kyc",
"host": ["{{base_url}}"],
"path": ["clients", "{{client_id}}", "kyc"]
},
"description": "Initiate a KYC record for a non-individual client with CIN and DIN numbers.\n\n- kycType: 2 = NON_INDIVIDUAL\n- riskCategory: 1=LOW, 2=MEDIUM, 3=HIGH\n- cinNumber: 21-character Corporate Identification Number\n- dinNumber: 8-digit Director Identification Number"
},
"response": []
},
{
"name": "Update KYC — Add CIN & DIN",
"request": {
"method": "PUT",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
],
"body": {
"mode": "raw",
"raw": "{\n \"cinNumber\": \"{{cin_number}}\",\n \"dinNumber\": \"{{din_number}}\"\n}"
},
"url": {
"raw": "{{base_url}}/clients/{{client_id}}/kyc/{{kyc_id}}",
"host": ["{{base_url}}"],
"path": ["clients", "{{client_id}}", "kyc", "{{kyc_id}}"]
},
"description": "Update an existing KYC record to add CIN and/or DIN numbers before triggering verification."
},
"response": []
}
],
"description": "Create or update a KYC record with CIN and DIN numbers before triggering verification."
},
{
"name": "2. CIN Verification (Company)",
"item": [
{
"name": "Verify CIN — Trigger LincPay validate-company",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test('CIN verified successfully', function () {",
" var json = pm.response.json();",
" pm.expect(json.changes).to.have.property('cinVerified');",
" pm.expect(json.changes.cinVerified).to.be.true;",
"});",
"",
"pm.test('cinVerifiedOn date is set', function () {",
" var json = pm.response.json();",
" pm.expect(json.changes).to.have.property('cinVerifiedOn');",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
],
"body": {
"mode": "raw",
"raw": "{}"
},
"url": {
"raw": "{{base_url}}/clients/{{client_id}}/kyc/{{kyc_id}}?command=verify-cin",
"host": ["{{base_url}}"],
"path": ["clients", "{{client_id}}", "kyc", "{{kyc_id}}"],
"query": [
{ "key": "command", "value": "verify-cin" }
]
},
"description": "Trigger CIN verification via LincPay MCA API.\n\n**Backend flow:**\n1. Reads cinNumber from the KYC record\n2. Calls LincPay: POST https://ekyc.lincpay.in/EKYC/ekyc/validate-company\n Body: { \"apiKey\": \"...\", \"cin\": \"U12345MH2020PTC123456\", \"secretKey\": \"...\" }\n3. On success: marks cinVerified=true, cinVerifiedOn=today\n4. On failure: returns error with vendor message\n\n**Prerequisites:**\n- KYC record must exist with a cinNumber set\n- KYC must not be in REJECTED status"
},
"response": [
{
"name": "200 — CIN Verified Successfully",
"originalRequest": {
"method": "POST",
"header": [],
"body": { "mode": "raw", "raw": "{}" },
"url": { "raw": "{{base_url}}/clients/1/kyc/1?command=verify-cin" }
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"body": "{\n \"officeId\": 1,\n \"clientId\": 1,\n \"resourceId\": 1,\n \"changes\": {\n \"cinVerified\": true,\n \"cinVerifiedOn\": [2026, 4, 3],\n \"kycStatus\": 200\n }\n}"
},
{
"name": "400 — CIN Not Set",
"status": "Bad Request",
"code": 400,
"_postman_previewlanguage": "json",
"body": "{\n \"developerMessage\": \"CIN number is not set on this KYC record. Update the KYC record first.\",\n \"httpStatusCode\": \"503\",\n \"defaultUserMessage\": \"CIN number is not set on this KYC record. Update the KYC record first.\",\n \"userMessageGlobalisationCode\": \"error.msg.client.kyc.cin.not.found\"\n}"
}
]
},
{
"name": "Direct LincPay CIN API (for testing)",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "accept", "value": "*/*" }
],
"body": {
"mode": "raw",
"raw": "{\n \"apiKey\": \"YOUR_API_KEY\",\n \"cin\": \"{{cin_number}}\",\n \"secretKey\": \"YOUR_SECRET_KEY\"\n}"
},
"url": {
"raw": "https://ekyc.lincpay.in/EKYC/ekyc/validate-company",
"protocol": "https",
"host": ["ekyc", "lincpay", "in"],
"path": ["EKYC", "ekyc", "validate-company"]
},
"description": "Direct call to LincPay validate-company API for testing.\nReplace apiKey and secretKey with actual credentials."
},
"response": []
}
],
"description": "CIN (Corporate Identification Number) verification flow. CIN is a 21-character alphanumeric code allotted by MCA to companies registered under the Companies Act."
},
{
"name": "3. DIN Verification (Director)",
"item": [
{
"name": "Verify DIN — Trigger LincPay validate-director",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test('DIN verified successfully', function () {",
" var json = pm.response.json();",
" pm.expect(json.changes).to.have.property('dinVerified');",
" pm.expect(json.changes.dinVerified).to.be.true;",
"});",
"",
"pm.test('dinVerifiedOn date is set', function () {",
" var json = pm.response.json();",
" pm.expect(json.changes).to.have.property('dinVerifiedOn');",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
],
"body": {
"mode": "raw",
"raw": "{}"
},
"url": {
"raw": "{{base_url}}/clients/{{client_id}}/kyc/{{kyc_id}}?command=verify-din",
"host": ["{{base_url}}"],
"path": ["clients", "{{client_id}}", "kyc", "{{kyc_id}}"],
"query": [
{ "key": "command", "value": "verify-din" }
]
},
"description": "Trigger DIN verification via LincPay MCA API.\n\n**Backend flow:**\n1. Reads dinNumber from the KYC record\n2. Calls LincPay: POST https://ekyc.lincpay.in/EKYC/ekyc/validate-director\n Body: { \"apiKey\": \"...\", \"din\": \"12345678\", \"secretKey\": \"...\" }\n3. On success: marks dinVerified=true, dinVerifiedOn=today\n4. On failure: returns error with vendor message\n\n**Prerequisites:**\n- KYC record must exist with a dinNumber set\n- KYC must not be in REJECTED status"
},
"response": [
{
"name": "200 — DIN Verified Successfully",
"originalRequest": {
"method": "POST",
"header": [],
"body": { "mode": "raw", "raw": "{}" },
"url": { "raw": "{{base_url}}/clients/1/kyc/1?command=verify-din" }
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"body": "{\n \"officeId\": 1,\n \"clientId\": 1,\n \"resourceId\": 1,\n \"changes\": {\n \"dinVerified\": true,\n \"dinVerifiedOn\": [2026, 4, 3],\n \"kycStatus\": 200\n }\n}"
},
{
"name": "400 — DIN Not Set",
"status": "Bad Request",
"code": 400,
"_postman_previewlanguage": "json",
"body": "{\n \"developerMessage\": \"DIN number is not set on this KYC record. Update the KYC record first.\",\n \"httpStatusCode\": \"503\",\n \"defaultUserMessage\": \"DIN number is not set on this KYC record. Update the KYC record first.\",\n \"userMessageGlobalisationCode\": \"error.msg.client.kyc.din.not.found\"\n}"
}
]
},
{
"name": "Direct LincPay DIN API (for testing)",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "accept", "value": "*/*" }
],
"body": {
"mode": "raw",
"raw": "{\n \"apiKey\": \"YOUR_API_KEY\",\n \"din\": \"{{din_number}}\",\n \"secretKey\": \"YOUR_SECRET_KEY\"\n}"
},
"url": {
"raw": "https://ekyc.lincpay.in/EKYC/ekyc/validate-director",
"protocol": "https",
"host": ["ekyc", "lincpay", "in"],
"path": ["EKYC", "ekyc", "validate-director"]
},
"description": "Direct call to LincPay validate-director API for testing.\nReplace apiKey and secretKey with actual credentials."
},
"response": []
}
],
"description": "DIN (Director Identification Number) verification flow. DIN is an 8-digit unique number allotted by MCA to company directors."
},
{
"name": "4. Get KYC Record",
"item": [
{
"name": "Get KYC Record (check CIN/DIN verification status)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test('CIN fields present', function () {",
" var json = pm.response.json();",
" pm.expect(json).to.have.property('cinNumber');",
" pm.expect(json).to.have.property('cinVerified');",
"});",
"",
"pm.test('DIN fields present', function () {",
" var json = pm.response.json();",
" pm.expect(json).to.have.property('dinNumber');",
" pm.expect(json).to.have.property('dinVerified');",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
],
"url": {
"raw": "{{base_url}}/clients/{{client_id}}/kyc/{{kyc_id}}",
"host": ["{{base_url}}"],
"path": ["clients", "{{client_id}}", "kyc", "{{kyc_id}}"]
},
"description": "Retrieve the KYC record to verify CIN/DIN verification status after running the verify commands."
},
"response": []
}
]
}
]
}
@@ -0,0 +1,277 @@
i {
"info": {
"name": "Entity Classification - CBS Fineract",
"description": "Entity Classification module for non-individual client onboarding with 7 regulatory categories:\n1. Startup India (DPIIT)\n2. Mudra Borrower (PMMY)\n3. Export Oriented Unit (EOU)\n4. Import/Export (IEC/DGFT)\n5. Priority Sector Lending (PSL)\n6. Non-Priority Sector (NPS)\n7. MSME (Udyam Registration)",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"auth": {
"type": "basic",
"basic": [
{
"key": "username",
"value": "bankadmin",
"type": "string"
},
{
"key": "password",
"value": "LincPay@2026",
"type": "string"
}
]
},
"variable": [
{
"key": "baseUrl",
"value": "https://localhost:8443/fineract-provider/api/v1"
},
{
"key": "clientId",
"value": "49"
},
{
"key": "tenantId",
"value": "default"
}
],
"item": [
{
"name": "GET - Retrieve All Entity Classifications",
"request": {
"method": "GET",
"header": [
{
"key": "Fineract-Platform-TenantId",
"value": "{{tenantId}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/clients/{{clientId}}/entityclassification",
"host": ["{{baseUrl}}"],
"path": ["clients", "{{clientId}}", "entityclassification"]
},
"description": "Retrieves all 6 entity classification categories for the given client. Returns null for categories that have not been submitted yet."
}
},
{
"name": "POST - Save Startup India (DPIIT)",
"request": {
"method": "POST",
"header": [
{
"key": "Fineract-Platform-TenantId",
"value": "{{tenantId}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/clients/{{clientId}}/entityclassification/startup",
"host": ["{{baseUrl}}"],
"path": ["clients", "{{clientId}}", "entityclassification", "startup"]
},
"body": {
"mode": "raw",
"raw": "{\n \"enabled\": true,\n \"dpiitRecognitionNumber\": \"DIPP12345\",\n \"recognitionDate\": \"15 January 2024\",\n \"certificateExpiryDate\": \"14 January 2034\",\n \"entityStage\": \"GROWTH\",\n \"sectorAsDpiit\": \"Information Technology\",\n \"nicIndustryCode\": \"62011\",\n \"taxExemption80iac\": true,\n \"sidbiFund\": false,\n \"selfCertification\": true,\n \"fastTrackPatent\": true,\n \"govtTenderExemption\": true,\n \"creditGuaranteeCgss\": false,\n \"pslLoanLimit\": 5000000.00,\n \"pslSubCategory\": \"StartupIndia-IT\",\n \"locale\": \"en\",\n \"dateFormat\": \"dd MMMM yyyy\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"description": "Creates or updates Startup India (DPIIT) entity classification. Captures DPIIT recognition number, tax exemptions under Section 80IAC, SIDBI fund eligibility, and PSL limits."
}
},
{
"name": "POST - Save Mudra Borrower (PMMY)",
"request": {
"method": "POST",
"header": [
{
"key": "Fineract-Platform-TenantId",
"value": "{{tenantId}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/clients/{{clientId}}/entityclassification/mudra",
"host": ["{{baseUrl}}"],
"path": ["clients", "{{clientId}}", "entityclassification", "mudra"]
},
"body": {
"mode": "raw",
"raw": "{\n \"enabled\": true,\n \"mudraCategory\": \"KISHORE\",\n \"mudraLoanAccountNo\": \"MUDRA2024MH00567\",\n \"lendingInstitutionName\": \"State Bank of India\",\n \"sanctionedAmount\": 350000.00,\n \"disbursementDate\": \"01 March 2024\",\n \"loanPurpose\": \"Working Capital\",\n \"mudraCardNumber\": \"MC20240567\",\n \"currentOutstanding\": 280000.00,\n \"repaymentStatus\": \"REGULAR\",\n \"npaFlag\": false,\n \"locale\": \"en\",\n \"dateFormat\": \"dd MMMM yyyy\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"description": "Creates or updates Mudra Borrower (PMMY) entity classification. Supports SHISHU (up to Rs 50K), KISHORE (50K-5L), and TARUN (5L-10L) categories under Pradhan Mantri Mudra Yojana."
}
},
{
"name": "POST - Save Export Oriented Unit (EOU)",
"request": {
"method": "POST",
"header": [
{
"key": "Fineract-Platform-TenantId",
"value": "{{tenantId}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/clients/{{clientId}}/entityclassification/eou",
"host": ["{{baseUrl}}"],
"path": ["clients", "{{clientId}}", "entityclassification", "eou"]
},
"body": {
"mode": "raw",
"raw": "{\n \"enabled\": true,\n \"eouRegistrationNumber\": \"EOU/MH/2023/00456\",\n \"registeredDcZone\": \"SEEPZ-SEZ Mumbai\",\n \"bondLutNumber\": \"LUT/2024/MH/0089\",\n \"validityFrom\": \"01 April 2023\",\n \"validityTo\": \"31 March 2028\",\n \"gstinEou\": \"27AABCU9603R1ZM\",\n \"annualExportObligation\": 25000000.00,\n \"nfeTarget\": 5000000.00,\n \"softexFilingStatus\": \"FILED\",\n \"brcSubmissionStatus\": \"SUBMITTED\",\n \"adCategoryBankCode\": \"AD-I\",\n \"femaDeclarationDate\": \"15 April 2023\",\n \"locale\": \"en\",\n \"dateFormat\": \"dd MMMM yyyy\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"description": "Creates or updates Export Oriented Unit (EOU) entity classification. Captures EOU registration, Bond/LUT details, GSTIN, export obligations, NFE targets, and FEMA compliance."
}
},
{
"name": "POST - Save Import/Export (IEC/DGFT)",
"request": {
"method": "POST",
"header": [
{
"key": "Fineract-Platform-TenantId",
"value": "{{tenantId}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/clients/{{clientId}}/entityclassification/importexport",
"host": ["{{baseUrl}}"],
"path": ["clients", "{{clientId}}", "entityclassification", "importexport"]
},
"body": {
"mode": "raw",
"raw": "{\n \"enabled\": true,\n \"iecCode\": \"0305012345\",\n \"iecIssueDate\": \"10 June 2020\",\n \"iecStatus\": \"ACTIVE\",\n \"tradeType\": \"BOTH\",\n \"primaryHsCode\": \"8471\",\n \"keyTradingCountries\": \"USA, UAE, Singapore, Germany\",\n \"adCategoryBankCode\": \"AD-I\",\n \"ecgcPolicyNumber\": \"ECGC/WTP/2024/00123\",\n \"ecgcPolicyExpiry\": \"31 March 2025\",\n \"eefcAccountRequired\": true,\n \"pcfcFacilityRequired\": false,\n \"femaDeclarationObtained\": true,\n \"locale\": \"en\",\n \"dateFormat\": \"dd MMMM yyyy\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"description": "Creates or updates Import/Export (IEC/DGFT) entity classification. Captures Importer Exporter Code, HS codes, ECGC insurance, EEFC/PCFC requirements, and FEMA compliance."
}
},
{
"name": "POST - Save Priority Sector Lending (PSL)",
"request": {
"method": "POST",
"header": [
{
"key": "Fineract-Platform-TenantId",
"value": "{{tenantId}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/clients/{{clientId}}/entityclassification/psl",
"host": ["{{baseUrl}}"],
"path": ["clients", "{{clientId}}", "entityclassification", "psl"]
},
"body": {
"mode": "raw",
"raw": "{\n \"enabled\": true,\n \"pslCategory\": \"MICRO_ENTERPRISE\",\n \"pslSubCategory\": \"Manufacturing\",\n \"pslLoanPurposeCode\": \"ME01\",\n \"pslEligibilityBasis\": \"Udyam Registration - Micro Enterprise with investment up to Rs 1 Cr\",\n \"fiddReportingCode\": \"FIDD-ME-MFG\",\n \"pslSanctionedLimit\": 10000000.00,\n \"msmeClassification\": \"MICRO\",\n \"udyamRegistrationNumber\": \"UDYAM-MH-01-0012345\",\n \"udyamRegistrationDate\": \"20 July 2023\",\n \"annualTurnover\": 45000000.00,\n \"investmentPlantMachinery\": 8500000.00,\n \"weakerSectionFlag\": true,\n \"scStBorrowerFlag\": false,\n \"womenEntrepreneurFlag\": true,\n \"minorityCommunityFlag\": false,\n \"locale\": \"en\",\n \"dateFormat\": \"dd MMMM yyyy\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"description": "Creates or updates Priority Sector Lending (PSL) entity classification as per RBI Master Direction. Captures PSL category, MSME classification, Udyam registration, FIDD reporting codes, and weaker section flags."
}
},
{
"name": "POST - Save Non-Priority Sector (NPS)",
"request": {
"method": "POST",
"header": [
{
"key": "Fineract-Platform-TenantId",
"value": "{{tenantId}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/clients/{{clientId}}/entityclassification/nps",
"host": ["{{baseUrl}}"],
"path": ["clients", "{{clientId}}", "entityclassification", "nps"]
},
"body": {
"mode": "raw",
"raw": "{\n \"enabled\": true,\n \"industrySector\": \"Real Estate\",\n \"nicCode\": \"68100\",\n \"industryDescription\": \"Real estate activities with owned or leased property - Commercial\",\n \"annualRevenueBand\": \"50CR_TO_100CR\",\n \"riskCategory\": \"MEDIUM\",\n \"customerRiskScore\": 55,\n \"amlRiskClassification\": \"Medium Risk - Enhanced Due Diligence Required\",\n \"pepFlag\": false,\n \"amlWatchlistMatch\": false,\n \"adverseMediaFlag\": false\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"description": "Creates or updates Non-Priority Sector (NPS) entity classification. Captures industry details, NIC code, risk categorization, AML risk classification, PEP status, and adverse media flags."
}
},
{
"name": "POST - Save MSME (Udyam Registration)",
"request": {
"method": "POST",
"header": [
{
"key": "Fineract-Platform-TenantId",
"value": "{{tenantId}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/clients/{{clientId}}/entityclassification/msme",
"host": ["{{baseUrl}}"],
"path": ["clients", "{{clientId}}", "entityclassification", "msme"]
},
"body": {
"mode": "raw",
"raw": "{\n \"enabled\": true,\n \"udyamRegistrationNumber\": \"UDYAM-MP-23-0012345\",\n \"registrationDate\": \"15 March 2023\",\n \"registrationStatus\": \"ACTIVE\",\n \"msmeClassification\": \"SMALL\",\n \"enterpriseType\": \"MANUFACTURING\",\n \"nicActivityCode\": \"13111\",\n \"annualTurnover\": 12.50,\n \"investmentPlantMachinery\": 3.75,\n \"financialYearAssessment\": \"2024-25\",\n \"socialCategoryOwner\": \"GENERAL\",\n \"genderPrimaryOwner\": \"MALE\",\n \"noOfEmployees\": 45,\n \"womenLedEnterprise\": false,\n \"divyangPwdOwned\": false,\n \"dateFormat\": \"dd MMMM yyyy\",\n \"locale\": \"en\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"description": "Creates or updates MSME (Udyam Registration) entity classification. Captures Udyam number, MSME classification (Micro/Small/Medium), enterprise type, financial thresholds, owner/promoter details, and special flags (women-led, PWD owned)."
}
}
]
}
@@ -32,4 +32,8 @@ public interface GlAccountTypeMapper {
@Mapping(target = "description", source = "glAccountType.code")
EnumOptionData map(GLAccountType glAccountType);
default GLAccountType map(Integer value) {
return GLAccountType.fromInt(value);
}
}
@@ -647,6 +647,35 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder verifyCinClientKyc(final Long clientId, final Long kycId) {
this.actionName = "VERIFYCIN";
this.entityName = "CLIENTKYC";
this.entityId = kycId;
this.clientId = clientId;
this.href = "/clients/" + clientId + "/kyc/" + kycId + "?command=verify-cin";
return this;
}
public CommandWrapperBuilder verifyDinClientKyc(final Long clientId, final Long kycId) {
this.actionName = "VERIFYDIN";
this.entityName = "CLIENTKYC";
this.entityId = kycId;
this.clientId = clientId;
this.href = "/clients/" + clientId + "/kyc/" + kycId + "?command=verify-din";
return this;
}
// ── Form 60 ──────────────────────────────────────────────────────────────
public CommandWrapperBuilder createClientForm60(final Long clientId) {
this.actionName = "CREATE";
this.entityName = "CLIENTFORM60";
this.entityId = null;
this.clientId = clientId;
this.href = "/clients/" + clientId + "/form60";
return this;
}
public CommandWrapperBuilder createClient() {
this.actionName = "CREATE";
this.entityName = "CLIENT";
@@ -58,6 +58,7 @@ public class ClientApiConstants {
public static final String CLIENT_WITHDRAW_REASON = "ClientWithdrawReason";
public static final String GENDER = "Gender";
public static final String SALUTATION = "Salutation";
public static final String CLIENT_TYPE = "ClientType";
public static final String CLIENT_CLASSIFICATION = "ClientClassification";
@@ -72,8 +73,9 @@ public class ClientApiConstants {
public static final String dateFormatParamName = "dateFormat";
public static final String address = "address";
public static final String familyMembers = "familyMembers";
public static final String MARITALSTATUS = "MARITALSTATUS";
public static final String MARITAL_STATUS = "MaritalStatus";
public static final String maritalStatusIdParamName = "maritalStatusId";
public static final String maritalStatusParamName = "maritalStatus";
public static final String CLIENT_TYPE_INDIVIDUAL = "Individual";
@@ -84,6 +86,7 @@ public class ClientApiConstants {
public static final String cifNumberParamName = "cifNumber";
public static final String externalIdParamName = "externalId";
public static final String mobileNoParamName = "mobileNo";
public static final String alternateMobileNoParamName = "alternateMobileNo";
public static final String emailAddressParamName = "emailAddress";
public static final String firstnameParamName = "firstname";
public static final String middlenameParamName = "middlename";
@@ -112,6 +115,8 @@ public class ClientApiConstants {
public static final String dateOfBirthParamName = "dateOfBirth";
public static final String genderIdParamName = "genderId";
public static final String genderParamName = "gender";
public static final String salutationIdParamName = "salutationId";
public static final String salutationParamName = "salutation";
public static final String clientTypeIdParamName = "clientTypeId";
public static final String clientTypeParamName = "clientType";
public static final String clientClassificationIdParamName = "clientClassificationId";
@@ -120,6 +125,24 @@ public class ClientApiConstants {
public static final String occupationCategoryParamName = "occupationCategory";
public static final String occupationIdParamName = "occupationId";
public static final String occupationParamName = "occupation";
public static final String monthlyIncomeParamName = "monthlyIncome";
public static final String annualTurnoverParamName = "annualTurnover";
public static final String ageYearsParamName = "ageYears";
public static final String ageMonthsParamName = "ageMonths";
public static final String ageDaysParamName = "ageDays";
public static final String nationalityIdParamName = "nationalityId";
public static final String isMinorParamName = "isMinor";
public static final String guardianClientIdParamName = "guardianClientId";
public static final String guardianClientDisplayNameParamName = "guardianClientDisplayName";
public static final String guardianRelationParamName = "guardianRelation";
public static final String dipVerifiedParamName = "dipVerified";
public static final String dipLookupLogIdParamName = "dipLookupLogId";
public static final String dipTimestampParamName = "dipTimestamp";
public static final String alternateDipVerifiedParamName = "alternateDipVerified";
public static final String alternateDipLookupLogIdParamName = "alternateDipLookupLogId";
public static final String alternateDipTimestampParamName = "alternateDipTimestamp";
public static final String nameVerifiedParamName = "nameVerified";
public static final String screeningLogIdParamName = "screeningLogId";
public static final String legalFormIdParamName = "legalFormId";
public static final String legalFormParamName = "legalForm";
// request parameters for payment details
@@ -137,6 +160,9 @@ public class ClientApiConstants {
public static final String incorpValidityTillParamName = "incorpValidityTillDate";
public static final String constitutionIdParamName = "constitutionId";
public static final String mainBusinessLineIdParamName = "mainBusinessLineId";
public static final String authorizedSignatoryParamName = "authorizedSignatory";
public static final String additionalInfoParamName = "additionalInfo";
public static final String dateOfRegistrationParamName = "dateOfRegistration";
// response parameters
public static final String statusParamName = "status";
@@ -206,7 +232,12 @@ public class ClientApiConstants {
officeNameParamName, transferToOfficeIdParamName, transferToOfficeNameParamName, hierarchyParamName, imageIdParamName,
imagePresentParamName, staffIdParamName, staffNameParamName, timelineParamName, groupsParamName, officeOptionsParamName,
staffOptionsParamName, dateOfBirthParamName, genderParamName, clientTypeParamName, clientClassificationParamName,
occupationCategoryParamName, occupationParamName,
salutationParamName, occupationCategoryParamName, occupationParamName, maritalStatusParamName, monthlyIncomeParamName,
annualTurnoverParamName, ageYearsParamName, ageMonthsParamName, ageDaysParamName,
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianClientDisplayNameParamName, guardianRelationParamName,
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
alternateMobileNoParamName, alternateDipVerifiedParamName, alternateDipLookupLogIdParamName, alternateDipTimestampParamName,
nameVerifiedParamName, screeningLogIdParamName,
legalFormParamName, clientNonPersonDetailsParamName, isStaffParamName, legalFormParamName));
protected static final Set<String> CLIENT_CHARGES_RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(chargeIdParamName,
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.client.data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
@@ -66,13 +67,34 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
private String fullname;
private String displayName;
private String mobileNo;
private String alternateMobileNo;
private String emailAddress;
private LocalDate dateOfBirth;
private CodeValueData gender;
private CodeValueData salutation;
private CodeValueData clientType;
private CodeValueData clientClassification;
private CodeValueData occupationCategory;
private CodeValueData occupation;
private CodeValueData maritalStatus;
private BigDecimal monthlyIncome;
private BigDecimal annualTurnover;
private Integer ageYears;
private Integer ageMonths;
private Integer ageDays;
private String nationality;
private Boolean isMinor;
private Long guardianClientId;
private String guardianClientDisplayName;
private String guardianRelation;
private Boolean dipVerified;
private Long dipLookupLogId;
private String dipTimestamp;
private Boolean alternateDipVerified;
private Long alternateDipLookupLogId;
private String alternateDipTimestamp;
private Boolean nameVerified;
private Long screeningLogId;
private Boolean isStaff;
private Long officeId;
@@ -103,6 +125,7 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
private Collection<SavingsProductData> savingProductOptions;
private Collection<SavingsAccountData> savingAccountOptions;
private Collection<CodeValueData> genderOptions;
private Collection<CodeValueData> salutationOptions;
private Collection<CodeValueData> clientTypeOptions;
private Collection<CodeValueData> clientClassificationOptions;
private Collection<CodeValueData> occupationCategoryOptions;
@@ -284,7 +307,8 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
public static ClientData template(final Long officeId, final LocalDate joinedDate, final Collection<OfficeData> officeOptions,
final Collection<StaffData> staffOptions, final Collection<CodeValueData> narrations,
final Collection<CodeValueData> genderOptions, final Collection<SavingsProductData> savingProductOptions,
final Collection<CodeValueData> genderOptions, final Collection<CodeValueData> salutationOptions,
final Collection<SavingsProductData> savingProductOptions,
final Collection<CodeValueData> clientTypeOptions, final Collection<CodeValueData> clientClassificationOptions,
final Collection<CodeValueData> occupationCategoryOptions, final Collection<CodeValueData> occupationOptions,
final Collection<CodeValueData> clientNonPersonConstitutionOptions,
@@ -330,6 +354,7 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
clientTypeOptions, clientClassificationOptions, clientNonPersonConstitutionOptions, clientNonPersonMainBusinessLineOptions,
clientNonPersonDetails, clientLegalFormOptions, familyMemberOptions, legalForm, address, isAddressEnabled, datatables,
isStaff, clientCollateralManagements);
data.salutationOptions = salutationOptions;
data.occupationCategoryOptions = occupationCategoryOptions;
data.occupationOptions = occupationOptions;
return data;
@@ -351,10 +376,32 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
templateData.familyMemberOptions, clientData.legalForm, clientData.address, clientData.isAddressEnabled, null,
clientData.isStaff, clientCollateralManagements);
data.cifNumber = clientData.cifNumber;
data.salutation = clientData.salutation;
data.salutationOptions = templateData.salutationOptions;
data.occupationCategory = clientData.occupationCategory;
data.occupation = clientData.occupation;
data.occupationCategoryOptions = templateData.occupationCategoryOptions;
data.occupationOptions = templateData.occupationOptions;
data.maritalStatus = clientData.maritalStatus;
data.monthlyIncome = clientData.monthlyIncome;
data.annualTurnover = clientData.annualTurnover;
data.ageYears = clientData.ageYears;
data.ageMonths = clientData.ageMonths;
data.ageDays = clientData.ageDays;
data.nationality = clientData.nationality;
data.isMinor = clientData.isMinor;
data.guardianClientId = clientData.guardianClientId;
data.guardianClientDisplayName = clientData.guardianClientDisplayName;
data.guardianRelation = clientData.guardianRelation;
data.alternateMobileNo = clientData.alternateMobileNo;
data.dipVerified = clientData.dipVerified;
data.dipLookupLogId = clientData.dipLookupLogId;
data.dipTimestamp = clientData.dipTimestamp;
data.alternateDipVerified = clientData.alternateDipVerified;
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
data.alternateDipTimestamp = clientData.alternateDipTimestamp;
data.nameVerified = clientData.nameVerified;
data.screeningLogId = clientData.screeningLogId;
return data;
}
@@ -375,10 +422,32 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
clientData.familyMemberOptions, clientData.legalForm, clientData.address, clientData.isAddressEnabled, null,
clientData.isStaff, clientCollateralManagements);
data.cifNumber = clientData.cifNumber;
data.salutation = clientData.salutation;
data.salutationOptions = clientData.salutationOptions;
data.occupationCategory = clientData.occupationCategory;
data.occupation = clientData.occupation;
data.occupationCategoryOptions = clientData.occupationCategoryOptions;
data.occupationOptions = clientData.occupationOptions;
data.maritalStatus = clientData.maritalStatus;
data.monthlyIncome = clientData.monthlyIncome;
data.annualTurnover = clientData.annualTurnover;
data.ageYears = clientData.ageYears;
data.ageMonths = clientData.ageMonths;
data.ageDays = clientData.ageDays;
data.nationality = clientData.nationality;
data.isMinor = clientData.isMinor;
data.guardianClientId = clientData.guardianClientId;
data.guardianClientDisplayName = clientData.guardianClientDisplayName;
data.guardianRelation = clientData.guardianRelation;
data.alternateMobileNo = clientData.alternateMobileNo;
data.dipVerified = clientData.dipVerified;
data.dipLookupLogId = clientData.dipLookupLogId;
data.dipTimestamp = clientData.dipTimestamp;
data.alternateDipVerified = clientData.alternateDipVerified;
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
data.alternateDipTimestamp = clientData.alternateDipTimestamp;
data.nameVerified = clientData.nameVerified;
data.screeningLogId = clientData.screeningLogId;
return data;
}
@@ -397,8 +466,29 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
clientData.clientNonPersonDetails, clientData.clientLegalFormOptions, clientData.familyMemberOptions, clientData.legalForm,
clientData.address, clientData.isAddressEnabled, null, clientData.isStaff, clientCollateralManagements);
data.cifNumber = clientData.cifNumber;
data.salutation = clientData.salutation;
data.occupationCategory = clientData.occupationCategory;
data.occupation = clientData.occupation;
data.maritalStatus = clientData.maritalStatus;
data.monthlyIncome = clientData.monthlyIncome;
data.annualTurnover = clientData.annualTurnover;
data.ageYears = clientData.ageYears;
data.ageMonths = clientData.ageMonths;
data.ageDays = clientData.ageDays;
data.nationality = clientData.nationality;
data.isMinor = clientData.isMinor;
data.guardianClientId = clientData.guardianClientId;
data.guardianClientDisplayName = clientData.guardianClientDisplayName;
data.guardianRelation = clientData.guardianRelation;
data.alternateMobileNo = clientData.alternateMobileNo;
data.dipVerified = clientData.dipVerified;
data.dipLookupLogId = clientData.dipLookupLogId;
data.dipTimestamp = clientData.dipTimestamp;
data.alternateDipVerified = clientData.alternateDipVerified;
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
data.alternateDipTimestamp = clientData.alternateDipTimestamp;
data.nameVerified = clientData.nameVerified;
data.screeningLogId = clientData.screeningLogId;
return data;
}
@@ -513,10 +603,11 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
final CodeValueData subStatus, final Long officeId, final String officeName, final Long transferToOfficeId,
final String transferToOfficeName, final Long id, final String firstname, final String middlename, final String lastname,
final String fullname, final String displayName, final ExternalId externalId, final String mobileNo,
final String emailAddress, final LocalDate dateOfBirth, final CodeValueData gender, final LocalDate activationDate,
final Long imageId, final Long staffId, final String staffName, final ClientTimelineData timeline,
final Long savingsProductId, final String savingsProductName, final Long savingsAccountId, final CodeValueData clientType,
final CodeValueData clientClassification, final CodeValueData occupationCategory, final CodeValueData occupation,
final String emailAddress, final LocalDate dateOfBirth, final CodeValueData gender, final CodeValueData salutation,
final LocalDate activationDate, final Long imageId, final Long staffId, final String staffName,
final ClientTimelineData timeline, final Long savingsProductId, final String savingsProductName,
final Long savingsAccountId, final CodeValueData clientType, final CodeValueData clientClassification,
final CodeValueData occupationCategory, final CodeValueData occupation,
final EnumOptionData legalForm, final ClientNonPersonData clientNonPerson, final Boolean isStaff) {
final Collection<OfficeData> allowedOffices = null;
@@ -538,6 +629,7 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
clientTypeOptions, clientClassificationOptions, clientNonPersonConstitutionOptions, clientNonPersonMainBusinessLineOptions,
clientNonPerson, clientLegalFormOptions, familyMemberOptions, legalForm, null, null, null, isStaff, null);
data.cifNumber = cifNumber;
data.salutation = salutation;
data.occupationCategory = occupationCategory;
data.occupation = occupation;
return data;
@@ -57,4 +57,11 @@ public class ClientFamilyMemberRequest implements Serializable {
private String nominationType;
private BigDecimal percentageShare;
private String remarks;
private String aadhaarNumber;
private String panNumber;
private String nationality;
private Boolean isAlive;
private Long linkedClientId;
private String marriageDate;
private String marriageAuthority;
}
@@ -73,6 +73,17 @@ public final class ClientFamilyMembersData implements Serializable {
private final String remarks;
// CBS extended fields
private final String aadhaarNumber;
private final String panNumber;
private final String nationality;
private final Boolean isAlive;
private final Long linkedClientId;
private final String linkedClientName;
private final String linkedClientCif;
private final LocalDate marriageDate;
private final String marriageAuthority;
// template holder
private final Collection<CodeValueData> relationshipIdOptions;
private final Collection<CodeValueData> genderIdOptions;
@@ -33,6 +33,9 @@ public class ClientNonPersonData implements Serializable {
private final LocalDate incorpValidityTillDate;
private final CodeValueData mainBusinessLine;
private final String remarks;
private final String authorizedSignatory;
private final String additionalInfo;
private final LocalDate dateOfRegistration;
// import fields
private Long mainBusinessLineId;
@@ -58,16 +61,23 @@ public class ClientNonPersonData implements Serializable {
this.locale = locale;
this.constitution = null;
this.mainBusinessLine = null;
this.authorizedSignatory = null;
this.additionalInfo = null;
this.dateOfRegistration = null;
}
public ClientNonPersonData(CodeValueData constitution, String incorpNo, LocalDate incorpValidityTillDate,
CodeValueData mainBusinessLine, String remarks) {
CodeValueData mainBusinessLine, String remarks, String authorizedSignatory, String additionalInfo,
LocalDate dateOfRegistration) {
this.constitution = constitution;
this.incorpNumber = incorpNo;
this.incorpValidityTillDate = incorpValidityTillDate;
this.mainBusinessLine = mainBusinessLine;
this.remarks = remarks;
this.authorizedSignatory = authorizedSignatory;
this.additionalInfo = additionalInfo;
this.dateOfRegistration = dateOfRegistration;
}
}
@@ -108,6 +108,9 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "mobile_no", length = 50, unique = true)
private String mobileNo;
@Column(name = "alternate_mobile_no", length = 50)
private String alternateMobileNo;
@Column(name = "email_address", length = 50, unique = true)
private String emailAddress;
@@ -124,6 +127,10 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@JoinColumn(name = "gender_cv_id")
private CodeValue gender;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "salutation_cv_id")
private CodeValue salutation;
@ManyToOne
@JoinColumn(name = "staff_id")
private Staff staff;
@@ -201,6 +208,53 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@JoinColumn(name = "occupation_cv_id")
private CodeValue occupation;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "marital_status_cv_id")
private CodeValue maritalStatus;
@Column(name = "monthly_income")
private java.math.BigDecimal monthlyIncome;
@Column(name = "annual_turnover")
private java.math.BigDecimal annualTurnover;
@Column(name = "nationality", length = 100)
private String nationality;
@Column(name = "is_minor")
private Boolean isMinor;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "guardian_client_id")
private Client guardianClient;
@Column(name = "guardian_relation", length = 50)
private String guardianRelation;
@Column(name = "dip_verified")
private Boolean dipVerified;
@Column(name = "dip_lookup_log_id")
private Long dipLookupLogId;
@Column(name = "dip_timestamp")
private String dipTimestamp;
@Column(name = "alternate_dip_verified")
private Boolean alternateDipVerified;
@Column(name = "alternate_dip_lookup_log_id")
private Long alternateDipLookupLogId;
@Column(name = "alternate_dip_timestamp")
private String alternateDipTimestamp;
@Column(name = "name_verified")
private Boolean nameVerified;
@Column(name = "screening_log_id")
private Long screeningLogId;
@Column(name = "legal_form_enum")
private Integer legalForm;
@@ -222,11 +276,12 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
final String fullname, final LocalDate activationDate, final LocalDate officeJoiningDate, final ExternalId externalId,
final String mobileNo, final String emailAddress, final Staff staff, final LocalDate submittedOnDate,
final Long savingsProductId, final Long savingsAccountId, final LocalDate dateOfBirth, final CodeValue gender,
final CodeValue clientType, final CodeValue clientClassification, final CodeValue occupationCategory,
final CodeValue occupation, final Integer legalForm, final Boolean isStaff) {
final CodeValue salutation, final CodeValue clientType, final CodeValue clientClassification,
final CodeValue occupationCategory, final CodeValue occupation, final Integer legalForm, final Boolean isStaff) {
return new Client(currentUser, status, office, clientParentGroup, accountNo, cifNumber, firstname, middlename, lastname, fullname,
activationDate, officeJoiningDate, externalId, mobileNo, emailAddress, staff, submittedOnDate, savingsProductId,
savingsAccountId, dateOfBirth, gender, clientType, clientClassification, occupationCategory, occupation, legalForm, isStaff);
savingsAccountId, dateOfBirth, gender, salutation, clientType, clientClassification, occupationCategory, occupation,
legalForm, isStaff);
}
protected Client() {}
@@ -236,8 +291,8 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
final String fullname, final LocalDate activationDate, final LocalDate officeJoiningDate, final ExternalId externalId,
final String mobileNo, final String emailAddress, final Staff staff, final LocalDate submittedOnDate,
final Long savingsProductId, final Long savingsAccountId, final LocalDate dateOfBirth, final CodeValue gender,
final CodeValue clientType, final CodeValue clientClassification, final CodeValue occupationCategory,
final CodeValue occupation, final Integer legalForm, final Boolean isStaff) {
final CodeValue salutation, final CodeValue clientType, final CodeValue clientClassification,
final CodeValue occupationCategory, final CodeValue occupation, final Integer legalForm, final Boolean isStaff) {
if (StringUtils.isBlank(accountNo)) {
this.accountNumber = new RandomPasswordGenerator(19).generate();
@@ -300,6 +355,7 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
if (gender != null) {
this.gender = gender;
}
this.salutation = salutation;
this.dateOfBirth = dateOfBirth;
@@ -486,7 +542,13 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
} else {
StringBuilder nameBuilder = new StringBuilder();
if (legalForm == null || LegalForm.fromInt(legalForm).isPerson()) {
if (this.salutation != null && StringUtils.isNotBlank(this.salutation.getLabel())) {
nameBuilder.append(this.salutation.getLabel());
}
if (StringUtils.isNotBlank(this.firstname)) {
if (!nameBuilder.isEmpty()) {
nameBuilder.append(' ');
}
nameBuilder.append(this.firstname);
}
if (StringUtils.isNotBlank(this.middlename)) {
@@ -632,6 +694,18 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return genderId;
}
public Long salutationId() {
Long salutationId = null;
if (this.salutation != null) {
salutationId = this.salutation.getId();
}
return salutationId;
}
public void updateSalutation(CodeValue salutation) {
this.salutation = salutation;
}
public Long clientTypeId() {
Long clientTypeId = null;
if (this.clientType != null) {
@@ -696,6 +770,18 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.occupation = occupation;
}
public Long maritalStatusId() {
Long maritalStatusId = null;
if (this.maritalStatus != null) {
maritalStatusId = this.maritalStatus.getId();
}
return maritalStatusId;
}
public void updateMaritalStatus(CodeValue maritalStatus) {
this.maritalStatus = maritalStatus;
}
public void updateGender(CodeValue gender) {
this.gender = gender;
}
@@ -738,6 +824,10 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.status = ClientStatus.PENDING.getValue();
}
public Integer getLegalForm() {
return this.legalForm;
}
public void setLegalForm(Integer legalForm) {
this.legalForm = legalForm;
}
@@ -0,0 +1,93 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* 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 java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/**
* Form 60 declaration as per Rule 114B of Income Tax Rules, 1962.
* Filed by individuals who do not have a PAN for specified financial transactions.
*/
@Entity
@Getter
@Table(name = "m_client_form60")
public class ClientForm60 extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@Column(name = "full_name", length = 200, nullable = false)
private String fullName;
@Column(name = "date_of_birth")
private LocalDate dateOfBirth;
@Column(name = "father_name", length = 200)
private String fatherName;
@Column(name = "address", length = 500)
private String address;
@Column(name = "transaction_type", length = 100, nullable = false)
private String transactionType;
@Column(name = "transaction_amount", scale = 6, precision = 19)
private BigDecimal transactionAmount;
@Column(name = "estimated_total_income", scale = 6, precision = 19)
private BigDecimal estimatedTotalIncome;
@Column(name = "reason_for_no_pan", length = 200)
private String reasonForNoPan;
@Column(name = "declaration", nullable = false)
private boolean declaration;
@Column(name = "submitted_on")
private LocalDate submittedOn;
protected ClientForm60() {}
public static ClientForm60 fromJson(final Client client, final JsonCommand command) {
final ClientForm60 form = new ClientForm60();
form.client = client;
form.fullName = command.stringValueOfParameterNamed("fullName");
form.dateOfBirth = command.localDateValueOfParameterNamed("dateOfBirth");
form.fatherName = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamed("fatherName"), null);
form.address = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamed("address"), null);
form.transactionType = command.stringValueOfParameterNamed("transactionType");
form.transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
form.estimatedTotalIncome = command.bigDecimalValueOfParameterNamed("estimatedTotalIncome");
form.reasonForNoPan = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamed("reasonForNoPan"), null);
form.declaration = command.booleanPrimitiveValueOfParameterNamed("declaration");
form.submittedOn = LocalDate.now();
return form;
}
}
@@ -0,0 +1,32 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.domain;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ClientForm60Repository extends JpaRepository<ClientForm60, Long> {
List<ClientForm60> findByClientId(Long clientId);
Optional<ClientForm60> findByIdAndClientId(Long id, Long clientId);
}
@@ -77,8 +77,14 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@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)
@Column(name = "pan_first_name", length = 200)
private String panFirstName;
@Column(name = "pan_last_name", length = 200)
private String panLastName;
/** LINKED, NOT_LINKED, Y, N, NA etc. (Aadhaar seeding status on PAN card) */
@Column(name = "pan_aadhaar_seeding_status", length = 20)
private String panAadhaarSeedingStatus;
/** PAN card category: individual, company, firm, etc. */
@@ -110,6 +116,87 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "gstin_verified_on")
private LocalDate gstinVerifiedOn;
// ── Enterprise / Non-Individual fields ──────────────────────────────────
@Column(name = "enterprise_pan", length = 10)
private String enterprisePan;
@Column(name = "cin_number", length = 21)
private String cinNumber;
@Column(name = "cin_verified", nullable = false)
private boolean cinVerified;
@Column(name = "cin_verified_on")
private LocalDate cinVerifiedOn;
// ── DIN (Director Identification Number) ────────────────────────────────
@Column(name = "din_number", length = 8)
private String dinNumber;
@Column(name = "din_verified", nullable = false)
private boolean dinVerified;
@Column(name = "din_verified_on")
private LocalDate dinVerifiedOn;
@Column(name = "incorporation_date")
private LocalDate incorporationDate;
// ── KYC Document ─────────────────────────────────────────────────────────
@Column(name = "kyc_document_name", length = 100)
private String kycDocumentName;
@Column(name = "kyc_document_number", length = 50)
private String kycDocumentNumber;
@Column(name = "kyc_doc_issue_date")
private LocalDate kycDocIssueDate;
@Column(name = "kyc_doc_expiry_date")
private LocalDate kycDocExpiryDate;
// ── AML / Compliance flags ───────────────────────────────────────────────
@Column(name = "aml_check_status", length = 20)
private String amlCheckStatus;
@Column(name = "banned_entity_match", length = 10)
private String bannedEntityMatch;
@Column(name = "willful_defaulter", length = 10)
private String willfulDefaulter;
@Column(name = "ecgc_sal_flag", length = 10)
private String ecgcSalFlag;
@Column(name = "dormant_flag", length = 20)
private String dormantFlag;
// ── Account status / Freeze ──────────────────────────────────────────────
@Column(name = "account_status", length = 20)
private String accountStatus;
@Column(name = "freeze_reason", length = 200)
private String freezeReason;
@Column(name = "freeze_authority", length = 100)
private String freezeAuthority;
// ── Form 60 / Form 15G/15H ───────────────────────────────────────────────
@Column(name = "form60_type", length = 20)
private String form60Type;
@Column(name = "form60_date")
private LocalDate form60Date;
@Column(name = "form15_applicable")
private Boolean form15Applicable;
@Column(name = "form15_type", length = 10)
private String form15Type;
@Column(name = "form15_fy", length = 10)
private String form15Fy;
// ── CKYCRR ───────────────────────────────────────────────────────────────
/** Central KYC Records Registry number issued by CERSAI */
@Column(name = "ckyc_number", length = 14)
@@ -150,17 +237,21 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.panVerified = false;
this.aadhaarVerified = false;
this.gstinVerified = false;
this.cinVerified = false;
this.dinVerified = 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);
final ClientKyc kyc = new ClientKyc(client,
command.integerValueOfParameterNamed("kycType"),
command.integerValueOfParameterNamed("riskCategory"),
command.stringValueOfParameterNamed("panNumber"),
command.stringValueOfParameterNamed("aadhaarNumberMasked"),
command.stringValueOfParameterNamed("gstinNumber"),
command.stringValueOfParameterNamed("remarks"));
kyc.applyExtendedFields(command);
return kyc;
}
/**
@@ -172,6 +263,48 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return new ClientKyc(client, kycType, riskCategory, panNumber, aadhaarNumberMasked, gstinNumber, remarks);
}
private void applyExtendedFields(final JsonCommand command) {
this.panAadhaarSeedingStatus = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("panAadhaarSeedingStatus"), this.panAadhaarSeedingStatus);
this.enterprisePan = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("enterprisePan"), null);
this.cinNumber = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("cinNumber"), null);
this.dinNumber = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("dinNumber"), null);
this.incorporationDate = command.localDateValueOfParameterNamed("incorporationDate");
this.kycDocumentName = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("kycDocumentName"), null);
this.kycDocumentNumber = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("kycDocumentNumber"), null);
this.kycDocIssueDate = command.localDateValueOfParameterNamed("kycDocIssueDate");
this.kycDocExpiryDate = command.localDateValueOfParameterNamed("kycDocExpiryDate");
this.amlCheckStatus = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("amlCheckStatus"), null);
this.bannedEntityMatch = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("bannedEntityMatch"), null);
this.willfulDefaulter = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("willfulDefaulter"), null);
this.ecgcSalFlag = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("ecgcSalFlag"), null);
this.dormantFlag = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("dormantFlag"), null);
this.accountStatus = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("accountStatus"), null);
this.freezeReason = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("freezeReason"), null);
this.freezeAuthority = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("freezeAuthority"), null);
this.form60Type = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("form60Type"), null);
this.form60Date = command.localDateValueOfParameterNamed("form60Date");
this.form15Applicable = command.booleanObjectValueOfParameterNamed("form15Applicable");
this.form15Type = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("form15Type"), null);
this.form15Fy = StringUtils.defaultIfEmpty(
command.stringValueOfParameterNamed("form15FY"), null);
}
public Map<String, Object> update(final JsonCommand command) {
final Map<String, Object> actualChanges = new LinkedHashMap<>();
@@ -210,9 +343,72 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.remarks = StringUtils.defaultIfEmpty(newValue, null);
}
updateStringField(command, "panAadhaarSeedingStatus", this.panAadhaarSeedingStatus, actualChanges,
v -> this.panAadhaarSeedingStatus = v);
updateStringField(command, "enterprisePan", this.enterprisePan, actualChanges, v -> this.enterprisePan = v);
updateStringField(command, "cinNumber", this.cinNumber, actualChanges, v -> this.cinNumber = v);
updateStringField(command, "dinNumber", this.dinNumber, actualChanges, v -> this.dinNumber = v);
updateDateField(command, "incorporationDate", this.incorporationDate, actualChanges,
v -> this.incorporationDate = v);
updateStringField(command, "kycDocumentName", this.kycDocumentName, actualChanges,
v -> this.kycDocumentName = v);
updateStringField(command, "kycDocumentNumber", this.kycDocumentNumber, actualChanges,
v -> this.kycDocumentNumber = v);
updateDateField(command, "kycDocIssueDate", this.kycDocIssueDate, actualChanges,
v -> this.kycDocIssueDate = v);
updateDateField(command, "kycDocExpiryDate", this.kycDocExpiryDate, actualChanges,
v -> this.kycDocExpiryDate = v);
updateStringField(command, "amlCheckStatus", this.amlCheckStatus, actualChanges,
v -> this.amlCheckStatus = v);
updateStringField(command, "bannedEntityMatch", this.bannedEntityMatch, actualChanges,
v -> this.bannedEntityMatch = v);
updateStringField(command, "willfulDefaulter", this.willfulDefaulter, actualChanges,
v -> this.willfulDefaulter = v);
updateStringField(command, "ecgcSalFlag", this.ecgcSalFlag, actualChanges, v -> this.ecgcSalFlag = v);
updateStringField(command, "dormantFlag", this.dormantFlag, actualChanges, v -> this.dormantFlag = v);
updateStringField(command, "accountStatus", this.accountStatus, actualChanges, v -> this.accountStatus = v);
updateStringField(command, "freezeReason", this.freezeReason, actualChanges, v -> this.freezeReason = v);
updateStringField(command, "freezeAuthority", this.freezeAuthority, actualChanges,
v -> this.freezeAuthority = v);
updateStringField(command, "form60Type", this.form60Type, actualChanges, v -> this.form60Type = v);
updateDateField(command, "form60Date", this.form60Date, actualChanges, v -> this.form60Date = v);
updateBooleanField(command, "form15Applicable", this.form15Applicable, actualChanges,
v -> this.form15Applicable = v);
updateStringField(command, "form15Type", this.form15Type, actualChanges, v -> this.form15Type = v);
updateStringField(command, "form15FY", this.form15Fy, actualChanges, v -> this.form15Fy = v);
return actualChanges;
}
private void updateStringField(final JsonCommand command, final String param, final String current,
final Map<String, Object> changes, final java.util.function.Consumer<String> setter) {
if (command.isChangeInStringParameterNamed(param, current)) {
final String newValue = command.stringValueOfParameterNamed(param);
changes.put(param, newValue);
setter.accept(StringUtils.defaultIfEmpty(newValue, null));
}
}
private void updateDateField(final JsonCommand command, final String param, final LocalDate current,
final Map<String, Object> changes, final java.util.function.Consumer<LocalDate> setter) {
if (command.isChangeInLocalDateParameterNamed(param, current)) {
final LocalDate newValue = command.localDateValueOfParameterNamed(param);
changes.put(param, newValue);
setter.accept(newValue);
}
}
private void updateBooleanField(final JsonCommand command, final String param, final Boolean current,
final Map<String, Object> changes, final java.util.function.Consumer<Boolean> setter) {
if (command.parameterExists(param)) {
final Boolean newValue = command.booleanObjectValueOfParameterNamed(param);
if ((newValue == null && current != null) || (newValue != null && !newValue.equals(current))) {
changes.put(param, newValue);
setter.accept(newValue);
}
}
}
/** Called when documents are submitted and 3rd-party verification is initiated. */
public void markInProgress() {
this.kycStatus = ClientKycStatus.IN_PROGRESS.getValue();
@@ -237,11 +433,13 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.remarks = StringUtils.defaultIfEmpty(remarks, this.remarks);
}
public void markPanVerified(final String fullName, final String aadhaarSeedingStatus,
final String category, final String status) {
public void markPanVerified(final String fullName, final String firstName, final String lastName,
final String aadhaarSeedingStatus, final String category, final String status) {
this.panVerified = true;
this.panVerifiedOn = LocalDate.now();
this.panFullName = StringUtils.defaultIfEmpty(fullName, null);
this.panFirstName = StringUtils.defaultIfEmpty(firstName, null);
this.panLastName = StringUtils.defaultIfEmpty(lastName, null);
this.panAadhaarSeedingStatus = normalizeAadhaarSeedingStatus(aadhaarSeedingStatus);
this.panCategory = StringUtils.defaultIfEmpty(category, null);
this.panStatus = StringUtils.defaultIfEmpty(status, null);
@@ -272,6 +470,38 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.gstinVerifiedOn = LocalDate.now();
}
public void markCinVerified() {
this.cinVerified = true;
this.cinVerifiedOn = LocalDate.now();
}
public void markDinVerified() {
this.dinVerified = true;
this.dinVerifiedOn = LocalDate.now();
}
/**
* Checks if all required verifications are complete for this KYC type and auto-promotes
* to VERIFIED status if so.
* <ul>
* <li>INDIVIDUAL: PAN + Aadhaar both verified</li>
* <li>NON_INDIVIDUAL: PAN + GSTIN both verified</li>
* </ul>
*/
public boolean tryAutoVerify() {
final ClientKycType type = ClientKycType.fromInt(this.kycType);
boolean allDone = false;
if (type != null && type.isIndividual()) {
allDone = this.panVerified && this.aadhaarVerified;
} else if (type != null && type.isNonIndividual()) {
allDone = this.panVerified && this.gstinVerified;
}
if (allDone && !getKycStatus().isVerified()) {
markVerified(this.ckycNumber);
}
return allDone;
}
// ── Accessors ─────────────────────────────────────────────────────────────
public Client getClient() {
@@ -318,6 +548,14 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return panFullName;
}
public String getPanFirstName() {
return panFirstName;
}
public String getPanLastName() {
return panLastName;
}
public String getPanAadhaarSeedingStatus() {
return panAadhaarSeedingStatus;
}
@@ -377,4 +615,104 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
public String getRemarks() {
return remarks;
}
public String getEnterprisePan() {
return enterprisePan;
}
public String getCinNumber() {
return cinNumber;
}
public boolean isCinVerified() {
return cinVerified;
}
public LocalDate getCinVerifiedOn() {
return cinVerifiedOn;
}
public String getDinNumber() {
return dinNumber;
}
public boolean isDinVerified() {
return dinVerified;
}
public LocalDate getDinVerifiedOn() {
return dinVerifiedOn;
}
public LocalDate getIncorporationDate() {
return incorporationDate;
}
public String getKycDocumentName() {
return kycDocumentName;
}
public String getKycDocumentNumber() {
return kycDocumentNumber;
}
public LocalDate getKycDocIssueDate() {
return kycDocIssueDate;
}
public LocalDate getKycDocExpiryDate() {
return kycDocExpiryDate;
}
public String getAmlCheckStatus() {
return amlCheckStatus;
}
public String getBannedEntityMatch() {
return bannedEntityMatch;
}
public String getWillfulDefaulter() {
return willfulDefaulter;
}
public String getEcgcSalFlag() {
return ecgcSalFlag;
}
public String getDormantFlag() {
return dormantFlag;
}
public String getAccountStatus() {
return accountStatus;
}
public String getFreezeReason() {
return freezeReason;
}
public String getFreezeAuthority() {
return freezeAuthority;
}
public String getForm60Type() {
return form60Type;
}
public LocalDate getForm60Date() {
return form60Date;
}
public Boolean getForm15Applicable() {
return form15Applicable;
}
public String getForm15Type() {
return form15Type;
}
public String getForm15Fy() {
return form15Fy;
}
}
@@ -0,0 +1,95 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* 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.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/**
* Nominee for a client. Links to an existing client in the system.
* For Individual clients, nominee must be an existing client (no manual entry).
* A client can have multiple nominees with percentage share totalling 100%.
*/
@Entity
@Table(name = "m_client_nominee")
@Getter
@Setter
@NoArgsConstructor
public class ClientNominee extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private Client client;
/** The nominee — must be an existing client in CBS */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "nominee_client_id", nullable = false)
private Client nomineeClient;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "relationship_cv_id")
private CodeValue relationship;
@Column(name = "percentage_share", precision = 5, scale = 2)
private BigDecimal percentageShare;
@Column(name = "is_active")
private Boolean isActive;
/** If nominee is a minor, guardian details */
@Column(name = "nominee_is_minor")
private Boolean nomineeIsMinor;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "guardian_client_id")
private Client guardianClient;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "guardian_relationship_cv_id")
private CodeValue guardianRelationship;
@Column(name = "effective_date")
private LocalDate effectiveDate;
@Column(name = "remarks", length = 500)
private String remarks;
/** Auto-generated unique nominee identifier, e.g. NOM-SB00123456-01 */
@Column(name = "nominee_identifier", length = 50, unique = true)
private String nomineeIdentifier;
/** Account number this nominee is linked to */
@Column(name = "account_no", length = 50)
private String accountNo;
/** Account type prefix: SB, CB, FD, RD, LA */
@Column(name = "account_type", length = 10)
private String accountType;
}
@@ -0,0 +1,46 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.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 ClientNomineeRepository extends JpaRepository<ClientNominee, Long> {
@Query("SELECT n FROM ClientNominee n WHERE n.client.id = :clientId ORDER BY n.id")
List<ClientNominee> findAllByClientId(@Param("clientId") Long clientId);
@Query("SELECT n FROM ClientNominee n WHERE n.client.id = :clientId AND n.id = :nomineeId")
Optional<ClientNominee> findByClientIdAndId(@Param("clientId") Long clientId, @Param("nomineeId") Long nomineeId);
@Query("SELECT n FROM ClientNominee n WHERE n.client.id = :clientId AND n.isActive = true")
List<ClientNominee> findActiveByClientId(@Param("clientId") Long clientId);
@Query("SELECT n FROM ClientNominee n WHERE n.accountNo = :accountNo AND n.accountType = :accountType AND n.isActive = true")
List<ClientNominee> findActiveByAccountNoAndAccountType(@Param("accountNo") String accountNo, @Param("accountType") String accountType);
@Query("SELECT n FROM ClientNominee n WHERE n.client.id = :clientId AND n.accountNo = :accountNo AND n.accountType = :accountType ORDER BY n.id")
List<ClientNominee> findByClientIdAndAccountNoAndAccountType(@Param("clientId") Long clientId, @Param("accountNo") String accountNo, @Param("accountType") String accountType);
@Query("SELECT COUNT(n) FROM ClientNominee n WHERE n.accountNo = :accountNo AND n.accountType = :accountType")
long countByAccountNoAndAccountType(@Param("accountNo") String accountNo, @Param("accountType") String accountType);
}
@@ -26,8 +26,8 @@ import lombok.Getter;
@Getter
public enum LegalForm {
PERSON(1, "legalFormType.person", "Person"), //
ENTITY(2, "legalFormType.entity", "Entity"); //
PERSON(1, "legalFormType.individual", "Individual"), //
ENTITY(2, "legalFormType.nonIndividual", "Non-Individual"); //
private final Integer value;
private final String code;
@@ -32,6 +32,7 @@ public class SearchedClient {
private final String displayName;
private final ExternalId externalId;
private final String accountNumber;
private final String cifNumber;
private final Long officeId;
private final String officeName;
private final String mobileNo;
@@ -75,9 +75,9 @@ public class SearchingClientRepositoryImpl implements SearchingClientRepository
predicates.add(cb.like(o.get("hierarchy"), hierarchyLikeValue));
String searchLikeValue = "%" + searchText + "%";
predicates.add(cb.or(cb.like(r.get("accountNumber"), searchLikeValue), cb.like(r.get("displayName"), searchLikeValue),
cb.like(r.get("externalId"), searchLikeValue), cb.like(r.get("mobileNo"), searchLikeValue),
cb.like(identity.get("documentKey"), searchLikeValue)));
predicates.add(cb.or(cb.like(r.get("accountNumber"), searchLikeValue), cb.like(r.get("cifNumber"), searchLikeValue),
cb.like(r.get("displayName"), searchLikeValue), cb.like(r.get("externalId"), searchLikeValue),
cb.like(r.get("mobileNo"), searchLikeValue), cb.like(identity.get("documentKey"), searchLikeValue)));
return cb.and(predicates.toArray(new Predicate[0]));
};
@@ -87,8 +87,8 @@ public class SearchingClientRepositoryImpl implements SearchingClientRepository
query.orderBy(orders);
query.select(cb.construct(SearchedClient.class, root.get("id"), root.get("displayName"), root.get("externalId"),
root.get("accountNumber"), office.get("id"), office.get("name"), root.get("mobileNo"), root.get("status"),
root.get("activationDate"), root.get("createdDate")));
root.get("accountNumber"), root.get("cifNumber"), office.get("id"), office.get("name"), root.get("mobileNo"),
root.get("status"), root.get("activationDate"), root.get("createdDate")));
TypedQuery<SearchedClient> queryToExecute = entityManager.createQuery(query);
@@ -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.entityclassification.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 java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.portfolio.client.domain.Client;
/**
* Export Oriented Unit (EOU) entity classification.
* Captures EOU registration, export obligations, FEMA compliance, and AD bank details.
*/
@Entity
@Table(name = "m_client_eou")
@Getter
@Setter
@NoArgsConstructor
public class ClientEou extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@Column(name = "enabled")
private Boolean enabled;
@Column(name = "eou_registration_number", length = 50)
private String eouRegistrationNumber;
@Column(name = "registered_dc_zone", length = 50)
private String registeredDcZone;
@Column(name = "bond_lut_number", length = 50)
private String bondLutNumber;
@Column(name = "validity_from")
private LocalDate validityFrom;
@Column(name = "validity_to")
private LocalDate validityTo;
@Column(name = "gstin_eou", length = 15)
private String gstinEou;
@Column(name = "annual_export_obligation", precision = 19, scale = 2)
private BigDecimal annualExportObligation;
@Column(name = "nfe_target", precision = 19, scale = 2)
private BigDecimal nfeTarget;
@Column(name = "softex_filing_status", length = 20)
private String softexFilingStatus;
@Column(name = "brc_submission_status", length = 20)
private String brcSubmissionStatus;
@Column(name = "ad_category_bank_code", length = 20)
private String adCategoryBankCode;
@Column(name = "fema_declaration_date")
private LocalDate femaDeclarationDate;
}
@@ -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.entityclassification.domain;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ClientEouRepository extends JpaRepository<ClientEou, Long> {
Optional<ClientEou> findByClientId(Long clientId);
void deleteByClientId(Long clientId);
}
@@ -0,0 +1,87 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.entityclassification.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 java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.portfolio.client.domain.Client;
/**
* Import/Export (IEC / DGFT) entity classification.
* Captures Importer Exporter Code, trade details, ECGC insurance, and FEMA compliance.
*/
@Entity
@Table(name = "m_client_import_export")
@Getter
@Setter
@NoArgsConstructor
public class ClientImportExport extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@Column(name = "enabled")
private Boolean enabled;
@Column(name = "iec_code", length = 10)
private String iecCode;
@Column(name = "iec_issue_date")
private LocalDate iecIssueDate;
@Column(name = "iec_status", length = 20)
private String iecStatus;
@Column(name = "trade_type", length = 30)
private String tradeType;
@Column(name = "primary_hs_code", length = 20)
private String primaryHsCode;
@Column(name = "key_trading_countries", length = 200)
private String keyTradingCountries;
@Column(name = "ad_category_bank_code", length = 20)
private String adCategoryBankCode;
@Column(name = "ecgc_policy_number", length = 50)
private String ecgcPolicyNumber;
@Column(name = "ecgc_policy_expiry")
private LocalDate ecgcPolicyExpiry;
@Column(name = "eefc_account_required")
private Boolean eefcAccountRequired;
@Column(name = "pcfc_facility_required")
private Boolean pcfcFacilityRequired;
@Column(name = "fema_declaration_obtained")
private Boolean femaDeclarationObtained;
}
@@ -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.entityclassification.domain;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ClientImportExportRepository extends JpaRepository<ClientImportExport, Long> {
Optional<ClientImportExport> findByClientId(Long clientId);
void deleteByClientId(Long clientId);
}
@@ -0,0 +1,93 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* 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.entityclassification.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 java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.portfolio.client.domain.Client;
@Entity
@Table(name = "m_client_msme")
@Getter
@Setter
@NoArgsConstructor
public class ClientMsme extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@Column(name = "enabled")
private Boolean enabled;
// Udyam Registration
@Column(name = "udyam_registration_number", length = 30)
private String udyamRegistrationNumber;
@Column(name = "registration_date")
private LocalDate registrationDate;
@Column(name = "registration_status", length = 20)
private String registrationStatus;
@Column(name = "msme_classification", length = 20)
private String msmeClassification;
@Column(name = "enterprise_type", length = 30)
private String enterpriseType;
@Column(name = "nic_activity_code", length = 20)
private String nicActivityCode;
// Financial Thresholds
@Column(name = "annual_turnover", precision = 19, scale = 2)
private BigDecimal annualTurnover;
@Column(name = "investment_plant_machinery", precision = 19, scale = 2)
private BigDecimal investmentPlantMachinery;
@Column(name = "financial_year_assessment", length = 10)
private String financialYearAssessment;
// Owner / Promoter Details
@Column(name = "social_category_owner", length = 30)
private String socialCategoryOwner;
@Column(name = "gender_primary_owner", length = 20)
private String genderPrimaryOwner;
@Column(name = "no_of_employees")
private Integer noOfEmployees;
@Column(name = "women_led_enterprise")
private Boolean womenLedEnterprise;
@Column(name = "divyang_pwd_owned")
private Boolean divyangPwdOwned;
}
@@ -0,0 +1,32 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.entityclassification.domain;
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 ClientMsmeRepository extends JpaRepository<ClientMsme, Long> {
@Query("SELECT e FROM ClientMsme e WHERE e.client.id = :clientId")
Optional<ClientMsme> findByClientId(@Param("clientId") Long clientId);
void deleteByClientId(Long clientId);
}
@@ -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.client.entityclassification.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 java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.portfolio.client.domain.Client;
/**
* Mudra Borrower (PMMY) entity classification.
* Captures Pradhan Mantri Mudra Yojana loan details - SHISHU, KISHORE, or TARUN categories.
*/
@Entity
@Table(name = "m_client_mudra_borrower")
@Getter
@Setter
@NoArgsConstructor
public class ClientMudraBorrower extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@Column(name = "enabled")
private Boolean enabled;
@Column(name = "mudra_category", length = 20)
private String mudraCategory;
@Column(name = "mudra_loan_account_no", length = 50)
private String mudraLoanAccountNo;
@Column(name = "lending_institution_name", length = 100)
private String lendingInstitutionName;
@Column(name = "sanctioned_amount", precision = 19, scale = 2)
private BigDecimal sanctionedAmount;
@Column(name = "disbursement_date")
private LocalDate disbursementDate;
@Column(name = "loan_purpose", length = 50)
private String loanPurpose;
@Column(name = "mudra_card_number", length = 20)
private String mudraCardNumber;
@Column(name = "current_outstanding", precision = 19, scale = 2)
private BigDecimal currentOutstanding;
@Column(name = "repayment_status", length = 30)
private String repaymentStatus;
@Column(name = "npa_flag")
private Boolean npaFlag;
}
@@ -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.entityclassification.domain;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ClientMudraBorrowerRepository extends JpaRepository<ClientMudraBorrower, Long> {
Optional<ClientMudraBorrower> findByClientId(Long clientId);
void deleteByClientId(Long clientId);
}
@@ -0,0 +1,80 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.entityclassification.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 lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.portfolio.client.domain.Client;
/**
* Non-Priority Sector (NPS) entity classification.
* Captures industry details, risk categorization, AML classification, and PEP/watchlist flags.
*/
@Entity
@Table(name = "m_client_non_priority_sector")
@Getter
@Setter
@NoArgsConstructor
public class ClientNonPrioritySector extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@Column(name = "enabled")
private Boolean enabled;
@Column(name = "industry_sector", length = 50)
private String industrySector;
@Column(name = "nic_code", length = 10)
private String nicCode;
@Column(name = "industry_description", length = 200)
private String industryDescription;
@Column(name = "annual_revenue_band", length = 30)
private String annualRevenueBand;
@Column(name = "risk_category", length = 20)
private String riskCategory;
@Column(name = "customer_risk_score")
private Integer customerRiskScore;
@Column(name = "aml_risk_classification", length = 50)
private String amlRiskClassification;
@Column(name = "pep_flag")
private Boolean pepFlag;
@Column(name = "aml_watchlist_match")
private Boolean amlWatchlistMatch;
@Column(name = "adverse_media_flag")
private Boolean adverseMediaFlag;
}
@@ -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.entityclassification.domain;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ClientNonPrioritySectorRepository extends JpaRepository<ClientNonPrioritySector, Long> {
Optional<ClientNonPrioritySector> findByClientId(Long clientId);
void deleteByClientId(Long clientId);
}
@@ -0,0 +1,97 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.entityclassification.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 java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.portfolio.client.domain.Client;
/**
* Priority Sector Lending (PSL) entity classification as per RBI Master Direction.
* Captures PSL category, MSME/Udyam details, weaker section flags, and FIDD reporting codes.
*/
@Entity
@Table(name = "m_client_priority_sector")
@Getter
@Setter
@NoArgsConstructor
public class ClientPrioritySector extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@Column(name = "enabled")
private Boolean enabled;
@Column(name = "psl_category", length = 30)
private String pslCategory;
@Column(name = "psl_sub_category", length = 50)
private String pslSubCategory;
@Column(name = "psl_loan_purpose_code", length = 10)
private String pslLoanPurposeCode;
@Column(name = "psl_eligibility_basis", length = 100)
private String pslEligibilityBasis;
@Column(name = "fidd_reporting_code", length = 20)
private String fiddReportingCode;
@Column(name = "psl_sanctioned_limit", precision = 19, scale = 2)
private BigDecimal pslSanctionedLimit;
@Column(name = "msme_classification", length = 50)
private String msmeClassification;
@Column(name = "udyam_registration_number", length = 30)
private String udyamRegistrationNumber;
@Column(name = "udyam_registration_date")
private LocalDate udyamRegistrationDate;
@Column(name = "annual_turnover", precision = 19, scale = 2)
private BigDecimal annualTurnover;
@Column(name = "investment_plant_machinery", precision = 19, scale = 2)
private BigDecimal investmentPlantMachinery;
@Column(name = "weaker_section_flag")
private Boolean weakerSectionFlag;
@Column(name = "sc_st_borrower_flag")
private Boolean scStBorrowerFlag;
@Column(name = "women_entrepreneur_flag")
private Boolean womenEntrepreneurFlag;
@Column(name = "minority_community_flag")
private Boolean minorityCommunityFlag;
}
@@ -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.entityclassification.domain;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ClientPrioritySectorRepository extends JpaRepository<ClientPrioritySector, Long> {
Optional<ClientPrioritySector> findByClientId(Long clientId);
void deleteByClientId(Long clientId);
}
@@ -0,0 +1,94 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* 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.entityclassification.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 java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.portfolio.client.domain.Client;
/**
* Startup India (DPIIT) entity classification for non-individual clients.
* Captures DPIIT recognition details, tax exemptions, and priority sector lending info.
*/
@Entity
@Table(name = "m_client_startup_india")
@Getter
@Setter
@NoArgsConstructor
public class ClientStartupIndia extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@Column(name = "enabled")
private Boolean enabled;
@Column(name = "dpiit_recognition_number", length = 50)
private String dpiitRecognitionNumber;
@Column(name = "recognition_date")
private LocalDate recognitionDate;
@Column(name = "certificate_expiry_date")
private LocalDate certificateExpiryDate;
@Column(name = "entity_stage", length = 30)
private String entityStage;
@Column(name = "sector_as_dpiit", length = 50)
private String sectorAsDpiit;
@Column(name = "nic_industry_code", length = 20)
private String nicIndustryCode;
@Column(name = "tax_exemption_80iac")
private Boolean taxExemption80iac;
@Column(name = "sidbi_fund")
private Boolean sidbiFund;
@Column(name = "self_certification")
private Boolean selfCertification;
@Column(name = "fast_track_patent")
private Boolean fastTrackPatent;
@Column(name = "govt_tender_exemption")
private Boolean govtTenderExemption;
@Column(name = "credit_guarantee_cgss")
private Boolean creditGuaranteeCgss;
@Column(name = "psl_loan_limit", precision = 19, scale = 2)
private BigDecimal pslLoanLimit;
@Column(name = "psl_sub_category", length = 50)
private String pslSubCategory;
}
@@ -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.entityclassification.domain;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ClientStartupIndiaRepository extends JpaRepository<ClientStartupIndia, Long> {
Optional<ClientStartupIndia> findByClientId(Long clientId);
void deleteByClientId(Long clientId);
}
@@ -58,6 +58,7 @@ public class MobileLookupLog extends AbstractAuditableWithUTCDateTimeCustom<Long
private String lookupSource;
@Column(name = "client_id")
@lombok.Setter
private Long clientId;
@Column(name = "looked_up_on", nullable = false)
@@ -85,6 +85,7 @@ public class SanctionsScreeningLog extends AbstractAuditableWithUTCDateTimeCusto
public String getMatchedNames() { return matchedNames; }
public String getScreeningSource() { return screeningSource; }
public Long getClientId() { return clientId; }
public void setClientId(Long clientId) { this.clientId = clientId; }
public String getIpAddress() { return ipAddress; }
public OffsetDateTime getScreenedOn() { return screenedOn; }
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -19,6 +19,7 @@
package org.apache.fineract.infrastructure.core.service;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.configuration.data.SMTPCredentialsData;
import org.apache.fineract.infrastructure.configuration.service.ExternalServicesPropertiesReadPlatformService;
import org.apache.fineract.infrastructure.core.domain.EmailDetail;
@@ -27,6 +28,7 @@ import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class GmailBackedPlatformEmailService implements PlatformEmailService {
@@ -90,7 +92,7 @@ public class GmailBackedPlatformEmailService implements PlatformEmailService {
mailSender.send(message);
} catch (Exception e) {
throw new PlatformEmailSendException(e);
log.warn("Email sending failed for address [{}]. Skipping. Error: {}", emailDetails.getAddress(), e.getMessage());
}
}
}
@@ -22,6 +22,7 @@ import com.google.gson.JsonObject;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
@@ -33,6 +34,7 @@ import java.util.Set;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientAddress;
@Entity
@@ -47,6 +49,14 @@ public class Address extends AbstractPersistableCustom<Long> {
@OneToMany(mappedBy = "address", cascade = CascadeType.ALL)
private Set<ClientAddress> clientaddress;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id")
private Client client;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "address_type_id")
private CodeValue addressType;
@Column(name = "street")
private String street;
@@ -377,4 +387,20 @@ public class Address extends AbstractPersistableCustom<Long> {
this.updatedOn = updatedOn;
}
public Client getClient() {
return this.client;
}
public void setClient(Client client) {
this.client = client;
}
public CodeValue getAddressType() {
return this.addressType;
}
public void setAddressType(CodeValue addressType) {
this.addressType = addressType;
}
}
@@ -63,6 +63,8 @@ public class AddressWritePlatformServiceImpl implements AddressWritePlatformServ
final Client client = clientRepositoryWrapper.findOneWithNotFoundDetection(clientId);
final Address address = createAddress(jsonObject);
address.setClient(client);
address.setAddressType(addressTypeIdCodeValue);
addressRepository.save(address);
final ClientAddress clientAddress = createClientAddress(client, jsonObject, addressTypeIdCodeValue, address);
@@ -86,6 +88,8 @@ public class AddressWritePlatformServiceImpl implements AddressWritePlatformServ
final CodeValue addressTypeIdCodeValue = codeValueRepository.getReferenceById(addressTypeId);
final Address address = createAddress(jsonObject);
address.setClient(client);
address.setAddressType(addressTypeIdCodeValue);
addressRepository.save(address);
clientAddress = createClientAddress(client, jsonObject, addressTypeIdCodeValue, address);
@@ -0,0 +1,193 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* 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 static org.apache.fineract.util.StreamResponseUtil.DISPOSITION_TYPE_ATTACHMENT;
import static org.springframework.http.HttpHeaders.CONTENT_LENGTH;
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.InputStream;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.command.core.CommandPipeline;
import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorContext;
import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
import org.apache.fineract.infrastructure.documentmanagement.api.FileUploadValidator;
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCreateCommand;
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentDeleteCommand;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateRequest;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteRequest;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteResponse;
import org.apache.fineract.infrastructure.documentmanagement.service.DocumentReadPlatformService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.util.StreamResponseUtil;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.springframework.stereotype.Component;
/**
* REST API for managing documents attached to family members.
* Delegates to Fineract's generic document infrastructure (m_document table)
* using entityType = "family_members".
*/
@Path("/v1/clients/{clientId}/familymembers/{familyMemberId}/documents")
@Component
@Tag(name = "Client Family Member Documents", description = "Upload, list, download and delete documents for family members (e.g. marriage certificates)")
@RequiredArgsConstructor
public class ClientFamilyMemberDocumentApiResource {
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "FAMILYMEMBERDOCUMENT";
private static final String ENTITY_TYPE = "family_members";
private final PlatformSecurityContext context;
private final DocumentReadPlatformService documentReadPlatformService;
private final FileUploadValidator fileUploadValidator;
private final ContentDetectorManager contentDetectorManager;
private final CommandPipeline commandPipeline;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List all documents for a family member")
public List<DocumentData> retrieveAllDocuments(
@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
@PathParam("familyMemberId") @Parameter(description = "familyMemberId") final Long familyMemberId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
return documentReadPlatformService.retrieveAllDocuments(ENTITY_TYPE, familyMemberId);
}
@GET
@Path("/{documentId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a specific document for a family member")
public DocumentData getDocument(
@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
@PathParam("familyMemberId") @Parameter(description = "familyMemberId") final Long familyMemberId,
@PathParam("documentId") @Parameter(description = "documentId") final Long documentId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
return documentReadPlatformService.retrieveDocument(ENTITY_TYPE, familyMemberId, documentId);
}
@GET
@Path("/{documentId}/attachment")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_OCTET_STREAM })
@Operation(summary = "Download the file associated with a family member document")
public Response downloadFile(
@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
@PathParam("familyMemberId") @Parameter(description = "familyMemberId") final Long familyMemberId,
@PathParam("documentId") @Parameter(description = "documentId") final Long documentId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
final var content = documentReadPlatformService.retrieveDocumentContent(ENTITY_TYPE, familyMemberId, documentId);
return StreamResponseUtil
.ok(StreamResponseUtil.StreamResponseData.builder().type(content.getContentType()).fileName(content.getFileName())
.dispositionType(DISPOSITION_TYPE_ATTACHMENT).stream(content.getStream()).size(content.getSize()).build());
}
@POST
@Consumes({ MediaType.MULTIPART_FORM_DATA })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Upload a document for a family member", description = """
Upload a document (e.g. marriage certificate) for a family member using multipart form data.
Body parts:
- name: name or summary of the document
- description: description of the document
- file: the file to be uploaded
""")
public DocumentCreateResponse createDocument(
@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
@PathParam("familyMemberId") @Parameter(description = "familyMemberId") final Long familyMemberId,
@HeaderParam(CONTENT_LENGTH) final Long fileSize,
@FormDataParam("file") final InputStream is,
@FormDataParam("file") final FormDataContentDisposition fileDetails,
@FormDataParam("file") final FormDataBodyPart filePart,
@FormDataParam("name") final String name,
@FormDataParam("description") final String description) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME_FOR_PERMISSIONS);
fileUploadValidator.validate(fileSize, is, fileDetails, filePart);
final var command = new DocumentCreateCommand();
var type = Optional.ofNullable(filePart.getMediaType()).map(MediaType::toString)
.or(() -> Optional.of(contentDetectorManager
.detect(ContentDetectorContext.builder().fileName(fileDetails.getFileName()).build()).getMimeType()))
.orElse(APPLICATION_OCTET_STREAM_VALUE);
command.setPayload(DocumentCreateRequest.builder()
.entityId(familyMemberId)
.entityType(ENTITY_TYPE)
.name(name)
.description(description)
.fileName(fileDetails.getFileName())
.size(fileSize)
.type(type)
.stream(is)
.build());
final Supplier<DocumentCreateResponse> response = commandPipeline.send(command);
return response.get();
}
@DELETE
@Path("/{documentId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a family member document")
public DocumentDeleteResponse deleteDocument(
@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
@PathParam("familyMemberId") @Parameter(description = "familyMemberId") final Long familyMemberId,
@PathParam("documentId") @Parameter(description = "documentId") final Long documentId) {
this.context.authenticatedUser().validateHasDeletePermission(RESOURCE_NAME_FOR_PERMISSIONS);
final var command = new DocumentDeleteCommand();
command.setPayload(DocumentDeleteRequest.builder()
.id(documentId)
.entityId(familyMemberId)
.entityType(ENTITY_TYPE)
.build());
final Supplier<DocumentDeleteResponse> response = commandPipeline.send(command);
return response.get();
}
}
@@ -0,0 +1,103 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.data.ClientForm60Data;
import org.apache.fineract.portfolio.client.service.ClientForm60ReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/clients/{clientId}/form60")
@Component
@Tag(name = "Client Form 60", description = "Form 60 declaration as per Rule 114B of Income Tax Rules, 1962. "
+ "Filed by individuals who do not have a PAN for specified financial transactions.")
@RequiredArgsConstructor
public class ClientForm60ApiResource {
private static final String RESOURCE_NAME = "CLIENTFORM60";
private final PlatformSecurityContext context;
private final ClientForm60ReadPlatformService readPlatformService;
private final DefaultToApiJsonSerializer<ClientForm60Data> toApiJsonSerializer;
private final DefaultToApiJsonSerializer<List> listSerializer;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Submit Form 60 for a Client",
description = "Creates a new Form 60 declaration for the given client.")
public String createForm60(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder()
.createClientForm60(clientId)
.withJson(apiRequestBodyAsJson)
.build();
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
return toApiJsonSerializer.serialize(result);
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve all Form 60 declarations for a Client",
description = "Returns all Form 60 records for the given client.")
public String retrieveAllForm60(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId) {
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME);
final List<ClientForm60Data> data = readPlatformService.retrieveAllByClientId(clientId);
return listSerializer.serialize(data);
}
@GET
@Path("{form60Id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a specific Form 60 by ID",
description = "Returns the Form 60 record identified by {form60Id} for the given client.")
public String retrieveForm60(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@PathParam("form60Id") @Parameter(description = "Form 60 record identifier") final Long form60Id) {
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME);
final ClientForm60Data data = readPlatformService.retrieveOne(clientId, form60Id);
return toApiJsonSerializer.serialize(data);
}
}
@@ -34,6 +34,10 @@ import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
@@ -43,10 +47,12 @@ import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSer
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.apache.fineract.portfolio.client.data.ClientKycRequest;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.service.ClientKycReadPlatformService;
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;
/**
@@ -80,6 +86,7 @@ public class ClientKycApiResource {
private final ClientKycReadPlatformService readPlatformService;
private final DefaultToApiJsonSerializer<ClientKycData> toApiJsonSerializer;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final ClientRepositoryWrapper clientRepository;
// ─────────────────────────────────────────────────────────────────────────
// INITIATE KYC – POST /v1/clients/{clientId}/kyc
@@ -103,16 +110,131 @@ public class ClientKycApiResource {
@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) {
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
LOG.info("KYC INITIATE received: clientId={}, requestBody={}", clientId, apiRequestBodyAsJson);
final CommandWrapper commandRequest = new CommandWrapperBuilder()
.initiateClientKyc(clientId)
.withJson(toApiJsonSerializer.serialize(clientKycRequest))
.withJson(apiRequestBodyAsJson)
.build();
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
// ─────────────────────────────────────────────────────────────────────────
// TEMPLATE – GET /v1/clients/{clientId}/kyc/template
// ─────────────────────────────────────────────────────────────────────────
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve KYC document template for a Client",
description = "Returns the list of required KYC documents based on the client's type "
+ "(clientTypeId code value). Used by the UI to populate the KYC document dropdown.")
@ApiResponse(responseCode = "200", description = "OK")
public String retrieveKycTemplate(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId) {
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
final Map<String, Object> template = new LinkedHashMap<>();
template.put("clientId", clientId);
template.put("clientDisplayName", client.getDisplayName());
// Resolve client type label from code value
final String clientTypeName = client.clientType() != null ? client.clientType().getLabel() : null;
final Integer clientTypeId = client.clientType() != null ? client.clientType().getId().intValue() : null;
final Integer legalFormId = client.getLegalForm();
template.put("clientTypeId", clientTypeId);
template.put("clientTypeName", clientTypeName);
template.put("legalFormId", legalFormId);
template.put("requiredDocuments", getRequiredDocuments(clientTypeName, legalFormId));
return toApiJsonSerializer.serialize(template);
}
/**
* Returns required KYC documents based on client type (RBI Master Direction on KYC).
*/
private List<Map<String, Object>> getRequiredDocuments(final String clientTypeName, final Integer legalFormId) {
final List<Map<String, Object>> docs = new ArrayList<>();
// Individual types (legalFormId=1)
if (legalFormId != null && legalFormId == 1) {
docs.add(doc("Aadhaar Card", "AADHAAR", true));
docs.add(doc("PAN Card", "PAN", true));
docs.add(doc("Voter ID", "VOTER_ID", false));
docs.add(doc("Driving License", "DRIVING_LICENSE", false));
docs.add(doc("Passport", "PASSPORT", false));
docs.add(doc("NREGA Job Card", "NREGA", false));
return docs;
}
// Non-individual types (legalFormId=2) – based on clientTypeName
if (clientTypeName == null) {
return docs;
}
final String type = clientTypeName.toUpperCase().trim();
if (type.contains("PROPRIETORSHIP")) {
docs.add(doc("PAN Card of Proprietor", "PAN", true));
docs.add(doc("GST Registration Certificate", "GST_CERT", true));
docs.add(doc("Shop & Establishment License", "SHOP_LICENSE", false));
docs.add(doc("Udyam Registration Certificate", "UDYAM", false));
docs.add(doc("Proprietor KYC - Aadhaar", "AADHAAR", true));
} else if (type.contains("PARTNERSHIP") || type.contains("LLP")) {
docs.add(doc("Registration Certificate", "REG_CERT", true));
docs.add(doc("Partnership Deed", "PARTNERSHIP_DEED", true));
docs.add(doc("PAN Card of Firm", "PAN", true));
docs.add(doc("KYC of Authorized Signatories", "SIGNATORY_KYC", true));
} else if (type.contains("PRIVATE") || type.contains("PUBLIC") || type.contains("CORPORATE")) {
docs.add(doc("Certificate of Incorporation", "COI", true));
docs.add(doc("Memorandum of Association", "MOA", true));
docs.add(doc("Articles of Association", "AOA", true));
docs.add(doc("PAN Card of Company", "PAN", true));
docs.add(doc("Board Resolution", "BOARD_RESOLUTION", true));
docs.add(doc("KYC of Authorized Signatories", "SIGNATORY_KYC", true));
} else if (type.contains("TRUST")) {
docs.add(doc("Registration Certificate", "REG_CERT", true));
docs.add(doc("Trust Deed", "TRUST_DEED", true));
docs.add(doc("PAN Card of Trust", "PAN", true));
docs.add(doc("KYC of Trustees / Authorized Signatories", "SIGNATORY_KYC", true));
} else if (type.contains("SOCIETY")) {
docs.add(doc("Registration Certificate", "REG_CERT", true));
docs.add(doc("PAN Card of Society", "PAN", true));
docs.add(doc("KYC of Authorized Signatories", "SIGNATORY_KYC", true));
} else if (type.contains("GOVERNMENT")) {
docs.add(doc("Government Order / Notification", "GOVT_ORDER", true));
docs.add(doc("PAN Card", "PAN", true));
docs.add(doc("KYC of Authorized Signatories", "SIGNATORY_KYC", true));
} else if (type.contains("MSME")) {
docs.add(doc("Udyam Registration Certificate", "UDYAM", true));
docs.add(doc("PAN Card", "PAN", true));
docs.add(doc("GST Registration Certificate", "GST_CERT", false));
docs.add(doc("KYC of Authorized Signatories", "SIGNATORY_KYC", true));
} else {
// Default non-individual
docs.add(doc("PAN Card", "PAN", true));
docs.add(doc("Registration Certificate", "REG_CERT", false));
docs.add(doc("KYC of Authorized Signatories", "SIGNATORY_KYC", false));
}
return docs;
}
private Map<String, Object> doc(final String name, final String code, final boolean mandatory) {
final Map<String, Object> d = new LinkedHashMap<>();
d.put("name", name);
d.put("code", code);
d.put("mandatory", mandatory);
return d;
}
// ─────────────────────────────────────────────────────────────────────────
// GET KYC for client – GET /v1/clients/{clientId}/kyc
// ─────────────────────────────────────────────────────────────────────────
@@ -173,11 +295,11 @@ public class ClientKycApiResource {
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) {
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder()
.updateClientKyc(clientId, kycId)
.withJson(toApiJsonSerializer.serialize(clientKycRequest))
.withJson(apiRequestBodyAsJson)
.build();
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
@@ -224,11 +346,11 @@ public class ClientKycApiResource {
@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")
description = "Command: aadhaar-generate-otp | verify-pan | verify-aadhaar | verify-gstin | verify-cin | verify-din | verify | reject")
final String command,
@Parameter(hidden = true) final ClientKycRequest clientKycRequest) {
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final String json = toApiJsonSerializer.serialize(clientKycRequest);
final String json = apiRequestBodyAsJson;
LOG.info("KYC command received: clientId={}, kycId={}, command={}, requestBody={}", clientId, kycId, command, json);
@@ -245,6 +367,12 @@ public class ClientKycApiResource {
} else if ("verify-gstin".equalsIgnoreCase(command)) {
commandRequest = new CommandWrapperBuilder()
.verifyGstinClientKyc(clientId, kycId).withJson(json).build();
} else if ("verify-cin".equalsIgnoreCase(command)) {
commandRequest = new CommandWrapperBuilder()
.verifyCinClientKyc(clientId, kycId).withJson(json).build();
} else if ("verify-din".equalsIgnoreCase(command)) {
commandRequest = new CommandWrapperBuilder()
.verifyDinClientKyc(clientId, kycId).withJson(json).build();
} else if ("verify".equalsIgnoreCase(command)) {
commandRequest = new CommandWrapperBuilder().verifyClientKyc(clientId, kycId).withJson(json).build();
} else if ("reject".equalsIgnoreCase(command)) {
@@ -252,7 +380,7 @@ public class ClientKycApiResource {
} else {
throw new org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException(
"command", command,
"aadhaar-generate-otp", "verify-pan", "verify-aadhaar", "verify-gstin", "verify", "reject");
"aadhaar-generate-otp", "verify-pan", "verify-aadhaar", "verify-gstin", "verify-cin", "verify-din", "verify", "reject");
}
try {
@@ -0,0 +1,171 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* 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 com.google.gson.JsonElement;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.data.ClientNomineeData;
import org.apache.fineract.portfolio.client.service.ClientNomineeReadPlatformService;
import org.apache.fineract.portfolio.client.service.ClientNomineeWritePlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/clients/{clientId}/nominees")
@Component
@Tag(name = "Client Nominees", description = "Manage nominees for a client's accounts. "
+ "Nominees are linked to specific accounts (SB/CB/FD/RD/LA), not to clients directly. "
+ "Nominee feature is only available if the client has at least one account. "
+ "Total percentage share per account cannot exceed 100%.")
@RequiredArgsConstructor
public class ClientNomineeApiResource {
private static final String RESOURCE_NAME = "CLIENTNOMINEE";
private final PlatformSecurityContext context;
private final ClientNomineeReadPlatformService readService;
private final ClientNomineeWritePlatformService writeService;
private final DefaultToApiJsonSerializer<ClientNomineeData> toApiJsonSerializer;
private final DefaultToApiJsonSerializer<List> listSerializer;
private final DefaultToApiJsonSerializer<CommandProcessingResult> resultSerializer;
private final FromJsonHelper fromJsonHelper;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve nominees for a client",
description = "Use accountNo + accountType query params to filter by a specific account. "
+ "Without these params, returns all nominees for the client.")
public String retrieveAll(
@PathParam("clientId") @Parameter(description = "Client ID") final Long clientId,
@QueryParam("accountNo") @Parameter(description = "Account number to filter by") final String accountNo,
@QueryParam("accountType") @Parameter(description = "Account type: SB, CB, FD, RD, LA") final String accountType) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME);
final List<ClientNomineeData> nominees;
if (accountNo != null && accountType != null) {
nominees = readService.retrieveNomineesByAccount(clientId, accountNo, accountType);
} else {
nominees = readService.retrieveAllNominees(clientId);
}
return listSerializer.serialize(nominees);
}
@GET
@Path("{nomineeId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a specific nominee")
public String retrieveOne(
@PathParam("clientId") @Parameter(description = "Client ID") final Long clientId,
@PathParam("nomineeId") @Parameter(description = "Nominee ID") final Long nomineeId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME);
final ClientNomineeData data = readService.retrieveNominee(clientId, nomineeId);
return toApiJsonSerializer.serialize(data);
}
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve nominee template with dropdown options",
description = "Also returns hasAnyAccount flag — nominee feature is only available if the client has accounts.")
public String retrieveTemplate(
@PathParam("clientId") @Parameter(description = "Client ID") final Long clientId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME);
final ClientNomineeData template = readService.retrieveTemplate(clientId);
template.setHasAnyAccount(readService.clientHasAnyAccount(clientId));
return toApiJsonSerializer.serialize(template);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Add a nominee to a client's account",
description = "accountNo and accountType are mandatory. Nominee must be an existing client. "
+ "Percentage share per account cannot exceed 100%.")
public String addNominee(
@PathParam("clientId") @Parameter(description = "Client ID") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
// Gate: client must have at least one account
if (!readService.clientHasAnyAccount(clientId)) {
throw new PlatformDataIntegrityException("error.msg.nominee.no.accounts",
"Cannot add nominees. Client " + clientId + " has no accounts (savings, current, FD, RD, or loan).",
"clientId");
}
final JsonElement parsedCommand = this.fromJsonHelper.parse(apiRequestBodyAsJson);
final JsonCommand command = new JsonCommand(null, apiRequestBodyAsJson, parsedCommand,
this.fromJsonHelper, "CLIENTNOMINEE", clientId, null, null, clientId,
null, null, null, null, null, null, null, null, null);
final CommandProcessingResult result = writeService.addNominee(clientId, command);
return resultSerializer.serialize(result);
}
@PUT
@Path("{nomineeId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update a nominee")
public String updateNominee(
@PathParam("clientId") @Parameter(description = "Client ID") final Long clientId,
@PathParam("nomineeId") @Parameter(description = "Nominee ID") final Long nomineeId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasUpdatePermission(RESOURCE_NAME);
final JsonElement parsedCommand = this.fromJsonHelper.parse(apiRequestBodyAsJson);
final JsonCommand command = new JsonCommand(null, apiRequestBodyAsJson, parsedCommand,
this.fromJsonHelper, "CLIENTNOMINEE", clientId, null, null, clientId,
null, null, null, null, null, null, null, null, null);
final CommandProcessingResult result = writeService.updateNominee(clientId, nomineeId, command);
return resultSerializer.serialize(result);
}
@DELETE
@Path("{nomineeId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a nominee")
public String deleteNominee(
@PathParam("clientId") @Parameter(description = "Client ID") final Long clientId,
@PathParam("nomineeId") @Parameter(description = "Nominee ID") final Long nomineeId) {
this.context.authenticatedUser().validateHasDeletePermission(RESOURCE_NAME);
final CommandProcessingResult result = writeService.deleteNominee(clientId, nomineeId);
return resultSerializer.serialize(result);
}
}
@@ -44,7 +44,9 @@ import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.data.AuditData;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.AuditReadPlatformService;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookPopulatorService;
@@ -56,6 +58,7 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.utils.SQLBuilder;
import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.Page;
@@ -74,6 +77,8 @@ import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Path("/v1/clients")
@@ -83,6 +88,8 @@ import org.springframework.stereotype.Component;
@RequiredArgsConstructor
public class ClientsApiResource {
private static final Logger LOG = LoggerFactory.getLogger(ClientsApiResource.class);
private final PlatformSecurityContext context;
private final ClientReadPlatformService clientReadPlatformService;
private final ClientTemplateReadPlatformService clientTemplateReadPlatformService;
@@ -96,6 +103,7 @@ public class ClientsApiResource {
private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService;
private final GuarantorReadPlatformService guarantorReadPlatformService;
private final SqlValidator sqlValidator;
private final AuditReadPlatformService auditReadPlatformService;
@GET
@Path("template")
@@ -180,6 +188,8 @@ public class ClientsApiResource {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = ClientsApiResourceSwagger.PostClientsResponse.class)))
public String create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
LOG.info("Client CREATE request body: {}", apiRequestBodyAsJson);
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
.createClient() //
.withJson(apiRequestBodyAsJson) //
@@ -588,4 +598,25 @@ public class ClientsApiResource {
return toApiJsonSerializer.serialize(transferDate);
}
@GET
@Path("{clientId}/audit")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve Client Activity Logs", description = "Returns audit trail for a specific client including all actions performed on the client and related entities.\n"
+ "\n" + "Example Requests:\n" + "\n" + "clients/1/audit\n" + "clients/1/audit?includeJson=true")
@ApiResponse(responseCode = "200", description = "OK")
public String retrieveClientAuditEntries(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
@Context final UriInfo uriInfo,
@QueryParam("includeJson") @Parameter(description = "includeJson") final Boolean includeJson) {
context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
final SQLBuilder extraCriteria = new SQLBuilder();
extraCriteria.addCriteria("aud.client_id = ", clientId);
final boolean includeJsonFlag = Boolean.TRUE.equals(includeJson);
final List<AuditData> auditEntries = auditReadPlatformService.retrieveAuditEntries(extraCriteria, includeJsonFlag);
return toApiJsonSerializer.serialize(auditEntries);
}
}
@@ -77,28 +77,39 @@ public class ClientKycCommand {
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()
// PAN – optional; validated only if provided
if (panNumber != null && !panNumber.isBlank()) {
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber)
.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()
// Aadhaar – optional; validated only if provided (mandatory for individual at verification time)
if (aadhaarNumberMasked != null && !aadhaarNumberMasked.isBlank()) {
v.reset().parameter(ClientKycApiConstants.aadhaarNumberMaskedParam).value(aadhaarNumberMasked)
.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()
// GSTIN – optional; validated only if provided (mandatory for non-individual at verification time)
if (gstinNumber != null && !gstinNumber.isBlank()) {
v.reset().parameter(ClientKycApiConstants.gstinNumberParam).value(gstinNumber)
.notExceedingLengthOf(15)
.matchesRegularExpression(ClientKycApiConstants.GSTIN_REGEX,
"GSTIN must be a valid 15-character GST Identification Number");
}
// At least one ID document must be provided
final boolean hasPan = panNumber != null && !panNumber.isBlank();
final boolean hasAadhaar = aadhaarNumberMasked != null && !aadhaarNumberMasked.isBlank();
final boolean hasGstin = gstinNumber != null && !gstinNumber.isBlank();
if (!hasPan && !hasAadhaar && !hasGstin) {
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber)
.notBlank();
}
throwExceptionIfValidationWarningsExist(errors);
}
@@ -184,6 +195,16 @@ public class ClientKycCommand {
// GSTIN is already stored on the KYC entity; nothing to validate in the command body.
}
/** verify-cin command: CIN is read from the existing KYC record – no required body fields. */
public void validateForVerifyCin() {
// CIN is already stored on the KYC entity; nothing to validate in the command body.
}
/** verify-din command: DIN is read from the existing KYC record – no required body fields. */
public void validateForVerifyDin() {
// DIN 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);
@@ -40,6 +40,7 @@ public class ClientAddressRequest implements Serializable {
private String addressLine2;
private String addressLine3;
private String townVillage;
private String street;
private String countyDistrict;
private Long stateProvinceId;
private BigDecimal latitude;
@@ -27,28 +27,40 @@ public class ClientApiCollectionConstants extends ClientApiConstants {
protected static final Set<String> CLIENT_CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(familyMembers, address,
localeParamName, dateFormatParamName, groupIdParamName, accountNoParamName, cifNumberParamName, externalIdParamName, mobileNoParamName,
emailAddressParamName, firstnameParamName, middlenameParamName, lastnameParamName, fullnameParamName, officeIdParamName,
alternateMobileNoParamName, emailAddressParamName, firstnameParamName, middlenameParamName, lastnameParamName, fullnameParamName, officeIdParamName,
activeParamName, activationDateParamName, staffIdParamName, submittedOnDateParamName, savingsProductIdParamName,
dateOfBirthParamName, genderIdParamName, clientTypeIdParamName, clientClassificationIdParamName,
occupationCategoryIdParamName, occupationIdParamName,
dateOfBirthParamName, genderIdParamName, salutationIdParamName, clientTypeIdParamName, clientClassificationIdParamName,
occupationCategoryIdParamName, occupationIdParamName, maritalStatusIdParamName, monthlyIncomeParamName, annualTurnoverParamName,
ageYearsParamName, ageMonthsParamName, ageDaysParamName,
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianRelationParamName,
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
alternateDipVerifiedParamName, alternateDipLookupLogIdParamName, alternateDipTimestampParamName,
nameVerifiedParamName, screeningLogIdParamName,
clientNonPersonDetailsParamName, displaynameParamName, legalFormIdParamName, datatables, isStaffParamName, kycParamName));
protected static final Set<String> CLIENT_NON_PERSON_CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(familyMembers, address, localeParamName, dateFormatParamName, incorpNumberParamName, remarksParamName,
incorpValidityTillParamName, constitutionIdParamName, mainBusinessLineIdParamName, datatables));
incorpValidityTillParamName, constitutionIdParamName, mainBusinessLineIdParamName,
authorizedSignatoryParamName, additionalInfoParamName, dateOfRegistrationParamName, datatables));
protected static final Set<String> CLIENT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName,
dateFormatParamName, accountNoParamName, externalIdParamName, mobileNoParamName, emailAddressParamName, firstnameParamName,
dateFormatParamName, accountNoParamName, externalIdParamName, mobileNoParamName, alternateMobileNoParamName, emailAddressParamName, firstnameParamName,
middlenameParamName,
lastnameParamName, fullnameParamName, activeParamName, activationDateParamName, staffIdParamName, savingsProductIdParamName,
dateOfBirthParamName, genderIdParamName, clientTypeIdParamName, clientClassificationIdParamName,
occupationCategoryIdParamName, occupationIdParamName,
dateOfBirthParamName, genderIdParamName, salutationIdParamName, clientTypeIdParamName, clientClassificationIdParamName,
occupationCategoryIdParamName, occupationIdParamName, maritalStatusIdParamName, monthlyIncomeParamName, annualTurnoverParamName,
ageYearsParamName, ageMonthsParamName, ageDaysParamName,
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianRelationParamName,
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
alternateDipVerifiedParamName, alternateDipLookupLogIdParamName, alternateDipTimestampParamName,
nameVerifiedParamName, screeningLogIdParamName,
submittedOnDateParamName, clientNonPersonDetailsParamName, displaynameParamName, legalFormIdParamName, isStaffParamName));
protected static final Set<String> CLIENT_NON_PERSON_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(localeParamName, dateFormatParamName, incorpNumberParamName, remarksParamName, incorpValidityTillParamName,
constitutionIdParamName, mainBusinessLineIdParamName));
constitutionIdParamName, mainBusinessLineIdParamName, authorizedSignatoryParamName, additionalInfoParamName,
dateOfRegistrationParamName));
/**
* These parameters will match the class level parameters of {@link ClientData}. Where possible, we try to get
@@ -166,6 +166,12 @@ public final class ClientDataValidator {
.notExceedingLengthOf(50);
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.alternateMobileNoParamName, element)) {
final String alternateMobileNo = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.alternateMobileNoParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.alternateMobileNoParamName).value(alternateMobileNo).ignoreIfNull()
.notExceedingLengthOf(50);
}
final Boolean active = this.fromApiJsonHelper.extractBooleanNamed(ClientApiConstants.activeParamName, element);
if (active != null) {
if (active.booleanValue()) {
@@ -225,6 +231,26 @@ public final class ClientDataValidator {
baseDataValidator.reset().parameter("isStaff").value(isStaffFlag).notNull();
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.nationalityIdParamName, element)) {
final String nationality = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.nationalityIdParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.nationalityIdParamName).value(nationality).notBlank();
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.isMinorParamName, element)) {
final Boolean isMinor = this.fromApiJsonHelper.extractBooleanNamed(ClientApiConstants.isMinorParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.isMinorParamName).value(isMinor).notNull();
if (Boolean.TRUE.equals(isMinor)) {
final Long guardianClientId = this.fromApiJsonHelper.extractLongNamed(ClientApiConstants.guardianClientIdParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.guardianClientIdParamName).value(guardianClientId).notNull()
.longGreaterThanZero();
final String guardianRelation = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.guardianRelationParamName,
element);
baseDataValidator.reset().parameter(ClientApiConstants.guardianRelationParamName).value(guardianRelation).notBlank();
}
}
if (this.configurationReadPlatformService.retrieveGlobalConfiguration(GlobalConfigurationConstants.ENABLE_ADDRESS).isEnabled()) {
final JsonArray address = this.fromApiJsonHelper.extractJsonArrayNamed(ClientApiConstants.address, element);
baseDataValidator.reset().parameter(ClientApiConstants.address).value(address).ignoreIfNull().jsonArrayNotEmpty();
@@ -276,6 +302,25 @@ public final class ClientDataValidator {
.integerGreaterThanZero();
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.authorizedSignatoryParamName, element)) {
final String authorizedSignatory = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.authorizedSignatoryParamName,
element);
baseDataValidator.reset().parameter(ClientApiConstants.authorizedSignatoryParamName).value(authorizedSignatory).ignoreIfNull()
.notExceedingLengthOf(500);
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.additionalInfoParamName, element)) {
final String additionalInfo = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.additionalInfoParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.additionalInfoParamName).value(additionalInfo).ignoreIfNull()
.notExceedingLengthOf(500);
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.dateOfRegistrationParamName, element)) {
final LocalDate dateOfRegistration = this.fromApiJsonHelper
.extractLocalDateNamed(ClientApiConstants.dateOfRegistrationParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.dateOfRegistrationParamName).value(dateOfRegistration).ignoreIfNull();
}
return dataValidationErrors;
}
@@ -443,6 +488,13 @@ public final class ClientDataValidator {
baseDataValidator.reset().parameter(ClientApiConstants.mobileNoParamName).value(mobileNo).notExceedingLengthOf(50);
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.alternateMobileNoParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
final String alternateMobileNo = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.alternateMobileNoParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.alternateMobileNoParamName).value(alternateMobileNo).ignoreIfNull()
.notExceedingLengthOf(50);
}
final Boolean active = this.fromApiJsonHelper.extractBooleanNamed(ClientApiConstants.activeParamName, element);
if (active != null) {
atLeastOneParameterPassedForUpdate = true;
@@ -513,6 +565,53 @@ public final class ClientDataValidator {
baseDataValidator.reset().parameter("isStaff").value(isStaffFlag).notNull();
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.nationalityIdParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
final String nationality = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.nationalityIdParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.nationalityIdParamName).value(nationality).notBlank();
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.isMinorParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
final Boolean isMinor = this.fromApiJsonHelper.extractBooleanNamed(ClientApiConstants.isMinorParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.isMinorParamName).value(isMinor).notNull();
if (Boolean.TRUE.equals(isMinor)) {
final Long guardianClientId = this.fromApiJsonHelper.extractLongNamed(ClientApiConstants.guardianClientIdParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.guardianClientIdParamName).value(guardianClientId).notNull()
.longGreaterThanZero();
final String guardianRelation = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.guardianRelationParamName,
element);
baseDataValidator.reset().parameter(ClientApiConstants.guardianRelationParamName).value(guardianRelation).notBlank();
}
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.dipVerifiedParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.dipLookupLogIdParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.dipTimestampParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.alternateDipVerifiedParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.alternateDipLookupLogIdParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.alternateDipTimestampParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.nameVerifiedParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.screeningLogIdParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
}
Map<String, Object> parameterUpdateStatusDetails = getParameterUpdateStatusAndDataValidationErrorsForUpdateOnClientNonPerson(
element.getAsJsonObject().get(ClientApiConstants.clientNonPersonDetailsParamName));
boolean atLeastOneParameterPassedForClientNonPersonUpdate = (boolean) parameterUpdateStatusDetails.get("parameterUpdateStatus");
@@ -573,6 +672,28 @@ public final class ClientDataValidator {
.integerGreaterThanZero();
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.authorizedSignatoryParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
final String authorizedSignatory = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.authorizedSignatoryParamName,
element);
baseDataValidator.reset().parameter(ClientApiConstants.authorizedSignatoryParamName).value(authorizedSignatory)
.notExceedingLengthOf(500);
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.additionalInfoParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
final String additionalInfo = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.additionalInfoParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.additionalInfoParamName).value(additionalInfo)
.notExceedingLengthOf(500);
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.dateOfRegistrationParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
final LocalDate dateOfRegistration = this.fromApiJsonHelper
.extractLocalDateNamed(ClientApiConstants.dateOfRegistrationParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.dateOfRegistrationParamName).value(dateOfRegistration);
}
Map<String, Object> parameterUpdateStatusDetails = new HashMap<>();
parameterUpdateStatusDetails.put("parameterUpdateStatus", atLeastOneParameterPassedForUpdate);
parameterUpdateStatusDetails.put("dataValidationErrors", dataValidationErrors);
@@ -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.client.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public final class ClientForm60Data {
private final Long id;
private final Long clientId;
private final String clientDisplayName;
private final String fullName;
private final LocalDate dateOfBirth;
private final String fatherName;
private final String address;
private final String transactionType;
private final BigDecimal transactionAmount;
private final BigDecimal estimatedTotalIncome;
private final String reasonForNoPan;
private final Boolean declaration;
private final LocalDate submittedOn;
}
@@ -48,14 +48,50 @@ public final class ClientKycApiConstants {
/** One-time password entered by the client – required for the verify-otp step. */
public static final String aadhaarOtpParam = "aadhaarOtp";
// ── Extended KYC fields ──────────────────────────────────────────────────
public static final String panAadhaarSeedingStatusParam = "panAadhaarSeedingStatus";
public static final String enterprisePanParam = "enterprisePan";
public static final String cinNumberParam = "cinNumber";
public static final String dinNumberParam = "dinNumber";
public static final String verifyCinParam = "verifyCin";
public static final String verifyDinParam = "verifyDin";
public static final String incorporationDateParam = "incorporationDate";
public static final String kycDocumentNameParam = "kycDocumentName";
public static final String kycDocumentNumberParam = "kycDocumentNumber";
public static final String kycDocIssueDateParam = "kycDocIssueDate";
public static final String kycDocExpiryDateParam = "kycDocExpiryDate";
public static final String amlCheckStatusParam = "amlCheckStatus";
public static final String bannedEntityMatchParam = "bannedEntityMatch";
public static final String willfulDefaulterParam = "willfulDefaulter";
public static final String ecgcSalFlagParam = "ecgcSalFlag";
public static final String dormantFlagParam = "dormantFlag";
public static final String accountStatusParam = "accountStatus";
public static final String freezeReasonParam = "freezeReason";
public static final String freezeAuthorityParam = "freezeAuthority";
public static final String form60TypeParam = "form60Type";
public static final String form60DateParam = "form60Date";
public static final String form15ApplicableParam = "form15Applicable";
public static final String form15TypeParam = "form15Type";
public static final String form15FYParam = "form15FY";
// ── Supported parameters for initiate/update ──────────────────────────────
public static final Set<String> INITIATE_REQUEST_PARAMS = new HashSet<>(Arrays.asList(
kycTypeParam, riskCategoryParam, panNumberParam, aadhaarNumberMaskedParam,
gstinNumberParam, remarksParam, "locale"));
gstinNumberParam, remarksParam, "locale", "dateFormat",
panAadhaarSeedingStatusParam, enterprisePanParam, cinNumberParam, dinNumberParam, incorporationDateParam,
kycDocumentNameParam, kycDocumentNumberParam, kycDocIssueDateParam, kycDocExpiryDateParam,
amlCheckStatusParam, bannedEntityMatchParam, willfulDefaulterParam, ecgcSalFlagParam,
dormantFlagParam, accountStatusParam, freezeReasonParam, freezeAuthorityParam,
form60TypeParam, form60DateParam, form15ApplicableParam, form15TypeParam, form15FYParam));
public static final Set<String> UPDATE_REQUEST_PARAMS = new HashSet<>(Arrays.asList(
panNumberParam, aadhaarNumberMaskedParam, gstinNumberParam,
riskCategoryParam, remarksParam, "locale"));
riskCategoryParam, remarksParam, "locale", "dateFormat",
panAadhaarSeedingStatusParam, enterprisePanParam, cinNumberParam, dinNumberParam, incorporationDateParam,
kycDocumentNameParam, kycDocumentNumberParam, kycDocIssueDateParam, kycDocExpiryDateParam,
amlCheckStatusParam, bannedEntityMatchParam, willfulDefaulterParam, ecgcSalFlagParam,
dormantFlagParam, accountStatusParam, freezeReasonParam, freezeAuthorityParam,
form60TypeParam, form60DateParam, form15ApplicableParam, form15TypeParam, form15FYParam));
/** verify command: existing flags + Aadhaar OTP fields for the verify-otp step. */
public static final Set<String> VERIFY_REQUEST_PARAMS = new HashSet<>(Arrays.asList(
@@ -76,6 +112,12 @@ public final class ClientKycApiConstants {
/** verify-gstin command: GSTIN is already stored; no extra body params required. */
public static final Set<String> VERIFY_GSTIN_REQUEST_PARAMS = new HashSet<>();
/** verify-cin command: CIN is already stored; no extra body params required. */
public static final Set<String> VERIFY_CIN_REQUEST_PARAMS = new HashSet<>();
/** verify-din command: DIN is already stored; no extra body params required. */
public static final Set<String> VERIFY_DIN_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 ────────
@@ -86,4 +128,10 @@ public final class ClientKycApiConstants {
// ── 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}$";
// ── CIN: 1 char status + 5 digit NIC + 2 char state + 4 digit year + 3 char ownership + 6 digit reg ──
public static final String CIN_REGEX = "^[A-Z]{1}[0-9]{5}[A-Z]{2}[0-9]{4}[A-Z]{3}[0-9]{6}$";
// ── DIN: 8-digit unique director identification number ──────────────────
public static final String DIN_REGEX = "^[0-9]{8}$";
}
@@ -46,6 +46,8 @@ public final class ClientKycData {
private final Boolean panVerified;
private final LocalDate panVerifiedOn;
private final String panFullName;
private final String panFirstName;
private final String panLastName;
private final String panAadhaarSeedingStatus;
private final String panCategory;
private final String panStatus;
@@ -60,6 +62,36 @@ public final class ClientKycData {
private final Boolean gstinVerified;
private final LocalDate gstinVerifiedOn;
// ── Enterprise / Non-Individual ──────────────────────────────────────────
private final String enterprisePan;
private final String cinNumber;
private final LocalDate incorporationDate;
// ── KYC Document ─────────────────────────────────────────────────────────
private final String kycDocumentName;
private final String kycDocumentNumber;
private final LocalDate kycDocIssueDate;
private final LocalDate kycDocExpiryDate;
// ── AML / Compliance ─────────────────────────────────────────────────────
private final String amlCheckStatus;
private final String bannedEntityMatch;
private final String willfulDefaulter;
private final String ecgcSalFlag;
private final String dormantFlag;
// ── Account status / Freeze ──────────────────────────────────────────────
private final String accountStatus;
private final String freezeReason;
private final String freezeAuthority;
// ── Form 60 / Form 15G/15H ───────────────────────────────────────────────
private final String form60Type;
private final LocalDate form60Date;
private final Boolean form15Applicable;
private final String form15Type;
private final String form15Fy;
// ── CKYCRR ────────────────────────────────────────────────────────────────
private final String ckycNumber;
@@ -48,6 +48,72 @@ public class ClientKycRequest {
@Schema(description = "15-character GSTIN – mandatory for NON_INDIVIDUAL clients", example = "27ABCDE1234F1Z5")
private String gstinNumber;
@Schema(description = "PAN-Aadhaar seeding/linking status", example = "LINKED")
private String panAadhaarSeedingStatus;
@Schema(description = "Enterprise PAN (for non-individual clients)", example = "ABCDE1234F")
private String enterprisePan;
@Schema(description = "Corporate Identification Number (21 characters)", example = "U12345MH2020PTC123456")
private String cinNumber;
@Schema(description = "Date of incorporation for non-individual clients", example = "01 April 2020")
private String incorporationDate;
@Schema(description = "Name of KYC document used for identification", example = "Aadhaar Card")
private String kycDocumentName;
@Schema(description = "KYC document number", example = "1234-5678-9012")
private String kycDocumentNumber;
@Schema(description = "KYC document issue date", example = "15 January 2020")
private String kycDocIssueDate;
@Schema(description = "KYC document expiry date (null if no expiry)", example = "null")
private String kycDocExpiryDate;
@Schema(description = "AML check status", example = "CLEAR")
private String amlCheckStatus;
@Schema(description = "Whether client matches banned entity list", example = "NO")
private String bannedEntityMatch;
@Schema(description = "Willful defaulter flag (RBI list)", example = "NO")
private String willfulDefaulter;
@Schema(description = "ECGC SAL (Specific Approval List) flag", example = "NO")
private String ecgcSalFlag;
@Schema(description = "Dormant account flag", example = "ACTIVE")
private String dormantFlag;
@Schema(description = "Account status", example = "ACTIVE")
private String accountStatus;
@Schema(description = "Reason for account freeze (null if not frozen)")
private String freezeReason;
@Schema(description = "Authority that ordered the freeze (null if not frozen)")
private String freezeAuthority;
@Schema(description = "Form 60 type (FORM_60 for clients without PAN)", example = "FORM_60")
private String form60Type;
@Schema(description = "Date of Form 60 submission")
private String form60Date;
@Schema(description = "Whether Form 15G/15H is applicable", example = "false")
private Boolean form15Applicable;
@Schema(description = "Form 15 type: 15G or 15H", example = "15G")
private String form15Type;
@Schema(description = "Financial year for Form 15G/15H")
private String form15FY;
@Schema(description = "Date format for date fields", example = "dd MMMM yyyy")
private String dateFormat;
@Schema(description = "Optional remarks / notes", example = "Documents submitted at branch")
private String remarks;
@@ -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.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
@Getter
@Setter
@NoArgsConstructor
public class ClientNomineeData {
private Long id;
private Long clientId;
// Nominee — linked existing client
private Long nomineeClientId;
private String nomineeDisplayName;
private String nomineeCifNumber;
private String nomineeAccountNo;
private LocalDate nomineeDateOfBirth;
private String nomineeMobileNo;
// Relationship
private Long relationshipId;
private String relationshipName;
// Share & status
private BigDecimal percentageShare;
private Boolean isActive;
// Minor nominee guardian
private Boolean nomineeIsMinor;
private Long guardianClientId;
private String guardianDisplayName;
private String guardianCifNumber;
private Long guardianRelationshipId;
private String guardianRelationshipName;
private LocalDate effectiveDate;
private String remarks;
// Nominee identifier (auto-generated)
private String nomineeIdentifier;
private String accountNo;
private String accountType;
// Template options
private Collection<CodeValueData> relationshipOptions;
/** Set on template response — true if client has at least one account (SB/CB/FD/RD/LA) */
private Boolean hasAnyAccount;
}
@@ -86,6 +86,29 @@ public class ClientFamilyMembers extends AbstractPersistableCustom<Long> {
@Column(name = "remarks")
private String remarks;
// ── CBS extended fields ──────────────────────────────────────────────
@Column(name = "aadhaar_number", length = 12)
private String aadhaarNumber;
@Column(name = "pan_number", length = 10)
private String panNumber;
@Column(name = "nationality", length = 50)
private String nationality;
@Column(name = "is_alive")
private Boolean isAlive;
@ManyToOne
@JoinColumn(name = "linked_client_id")
private Client linkedClient;
@Column(name = "marriage_date")
private LocalDate marriageDate;
@Column(name = "marriage_authority", length = 50)
private String marriageAuthority;
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 CodeValue maritalStatus, final CodeValue gender, final LocalDate dateOfBirth, final CodeValue profession,
@@ -107,6 +130,7 @@ public class ClientFamilyMembers extends AbstractPersistableCustom<Long> {
this.nominationType = nominationType;
this.percentageShare = percentageShare;
this.remarks = remarks;
this.isAlive = true;
}
public ClientFamilyMembers() {
@@ -249,4 +273,24 @@ public class ClientFamilyMembers extends AbstractPersistableCustom<Long> {
this.remarks = remarks;
}
public String getAadhaarNumber() { return aadhaarNumber; }
public void setAadhaarNumber(String aadhaarNumber) { this.aadhaarNumber = aadhaarNumber; }
public String getPanNumber() { return panNumber; }
public void setPanNumber(String panNumber) { this.panNumber = panNumber; }
public String getNationality() { return nationality; }
public void setNationality(String nationality) { this.nationality = nationality; }
public Boolean getIsAlive() { return isAlive; }
public void setIsAlive(Boolean isAlive) { this.isAlive = isAlive; }
public Client getLinkedClient() { return linkedClient; }
public void setLinkedClient(Client linkedClient) { this.linkedClient = linkedClient; }
public LocalDate getMarriageDate() { return marriageDate; }
public void setMarriageDate(LocalDate marriageDate) { this.marriageDate = marriageDate; }
public String getMarriageAuthority() { return marriageAuthority; }
public void setMarriageAuthority(String marriageAuthority) { this.marriageAuthority = marriageAuthority; }
}
@@ -64,9 +64,20 @@ public class ClientNonPerson extends AbstractPersistableCustom<Long> {
@Column(name = "remarks", length = 150, nullable = true)
private String remarks;
@Column(name = "authorized_signatory", length = 500, nullable = true)
private String authorizedSignatory;
@Column(name = "additional_info", length = 500, nullable = true)
private String additionalInfo;
@Column(name = "date_of_registration", nullable = true)
private LocalDate dateOfRegistration;
public static ClientNonPerson createNew(final Client client, final CodeValue constitution, final CodeValue mainBusinessLine,
String incorpNumber, LocalDate incorpValidityTill, String remarks) {
return new ClientNonPerson(client, constitution, mainBusinessLine, incorpNumber, incorpValidityTill, remarks);
String incorpNumber, LocalDate incorpValidityTill, String remarks, String authorizedSignatory, String additionalInfo,
LocalDate dateOfRegistration) {
return new ClientNonPerson(client, constitution, mainBusinessLine, incorpNumber, incorpValidityTill, remarks, authorizedSignatory,
additionalInfo, dateOfRegistration);
}
protected ClientNonPerson() {
@@ -74,7 +85,8 @@ public class ClientNonPerson extends AbstractPersistableCustom<Long> {
}
private ClientNonPerson(final Client client, final CodeValue constitution, final CodeValue mainBusinessLine, final String incorpNumber,
final LocalDate incorpValidityTill, final String remarks) {
final LocalDate incorpValidityTill, final String remarks, final String authorizedSignatory, final String additionalInfo,
final LocalDate dateOfRegistration) {
if (client != null) {
this.client = client;
}
@@ -97,6 +109,16 @@ public class ClientNonPerson extends AbstractPersistableCustom<Long> {
this.remarks = remarks.trim();
}
if (StringUtils.isNotBlank(authorizedSignatory)) {
this.authorizedSignatory = authorizedSignatory.trim();
}
if (StringUtils.isNotBlank(additionalInfo)) {
this.additionalInfo = additionalInfo.trim();
}
this.dateOfRegistration = dateOfRegistration;
validate(client);
}
@@ -192,6 +214,26 @@ public class ClientNonPerson extends AbstractPersistableCustom<Long> {
actualChanges.put(ClientApiConstants.mainBusinessLineIdParamName, newValue);
}
if (command.isChangeInStringParameterNamed(ClientApiConstants.authorizedSignatoryParamName, this.authorizedSignatory)) {
final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.authorizedSignatoryParamName);
actualChanges.put(ClientApiConstants.authorizedSignatoryParamName, newValue);
this.authorizedSignatory = StringUtils.defaultIfEmpty(newValue, null);
}
if (command.isChangeInStringParameterNamed(ClientApiConstants.additionalInfoParamName, this.additionalInfo)) {
final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.additionalInfoParamName);
actualChanges.put(ClientApiConstants.additionalInfoParamName, newValue);
this.additionalInfo = StringUtils.defaultIfEmpty(newValue, null);
}
if (command.isChangeInLocalDateParameterNamed(ClientApiConstants.dateOfRegistrationParamName, this.dateOfRegistration)) {
final String valueAsInput = command.stringValueOfParameterNamed(ClientApiConstants.dateOfRegistrationParamName);
actualChanges.put(ClientApiConstants.dateOfRegistrationParamName, valueAsInput);
actualChanges.put(ClientApiConstants.dateFormatParamName, dateFormatAsInput);
actualChanges.put(ClientApiConstants.localeParamName, localeAsInput);
this.dateOfRegistration = command.localDateValueOfParameterNamed(ClientApiConstants.dateOfRegistrationParamName);
}
// validate();
return actualChanges;
@@ -0,0 +1,191 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* 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.entityclassification.api;
import com.google.gson.JsonElement;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.entityclassification.data.EntityClassificationData;
import org.apache.fineract.portfolio.client.entityclassification.service.EntityClassificationReadService;
import org.apache.fineract.portfolio.client.entityclassification.service.EntityClassificationWriteService;
import org.springframework.stereotype.Component;
@Path("/v1/clients/{clientId}/entityclassification")
@Component
@Tag(name = "Entity Classification", description = "Entity Classification for non-individual client onboarding. "
+ "Supports 6 regulatory categories: Startup India (DPIIT), Mudra Borrower (PMMY), "
+ "Export Oriented Unit (EOU), Import/Export (IEC/DGFT), Priority Sector Lending (PSL), "
+ "Non-Priority Sector (NPS), and MSME (Udyam Registration).")
@RequiredArgsConstructor
public class EntityClassificationApiResource {
private static final String RESOURCE_NAME = "ENTITYCLASSIFICATION";
private final PlatformSecurityContext context;
private final EntityClassificationReadService readService;
private final EntityClassificationWriteService writeService;
private final DefaultToApiJsonSerializer<EntityClassificationData> toApiJsonSerializer;
private final DefaultToApiJsonSerializer<CommandProcessingResult> resultSerializer;
private final FromJsonHelper fromJsonHelper;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve all entity classifications for a client",
description = "Returns all 6 entity classification categories for the given client.")
public String retrieveAll(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME);
final EntityClassificationData data = readService.retrieveAllClassifications(clientId);
return toApiJsonSerializer.serialize(data);
}
@POST
@Path("startup")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save Startup India (DPIIT) classification",
description = "Creates or updates the Startup India entity classification for the given client.")
public String saveStartupIndia(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
final JsonCommand command = toJsonCommand(clientId, apiRequestBodyAsJson);
final CommandProcessingResult result = writeService.saveStartupIndia(clientId, command);
return resultSerializer.serialize(result);
}
@POST
@Path("mudra")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save Mudra Borrower (PMMY) classification",
description = "Creates or updates the Mudra Borrower entity classification for the given client.")
public String saveMudraBorrower(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
final JsonCommand command = toJsonCommand(clientId, apiRequestBodyAsJson);
final CommandProcessingResult result = writeService.saveMudraBorrower(clientId, command);
return resultSerializer.serialize(result);
}
@POST
@Path("eou")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save Export Oriented Unit (EOU) classification",
description = "Creates or updates the EOU entity classification for the given client.")
public String saveEou(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
final JsonCommand command = toJsonCommand(clientId, apiRequestBodyAsJson);
final CommandProcessingResult result = writeService.saveEou(clientId, command);
return resultSerializer.serialize(result);
}
@POST
@Path("importexport")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save Import/Export (IEC/DGFT) classification",
description = "Creates or updates the Import/Export entity classification for the given client.")
public String saveImportExport(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
final JsonCommand command = toJsonCommand(clientId, apiRequestBodyAsJson);
final CommandProcessingResult result = writeService.saveImportExport(clientId, command);
return resultSerializer.serialize(result);
}
@POST
@Path("psl")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save Priority Sector Lending (PSL) classification",
description = "Creates or updates the Priority Sector Lending entity classification for the given client.")
public String savePrioritySector(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
final JsonCommand command = toJsonCommand(clientId, apiRequestBodyAsJson);
final CommandProcessingResult result = writeService.savePrioritySector(clientId, command);
return resultSerializer.serialize(result);
}
@POST
@Path("nps")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save Non-Priority Sector (NPS) classification",
description = "Creates or updates the Non-Priority Sector entity classification for the given client.")
public String saveNonPrioritySector(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
final JsonCommand command = toJsonCommand(clientId, apiRequestBodyAsJson);
final CommandProcessingResult result = writeService.saveNonPrioritySector(clientId, command);
return resultSerializer.serialize(result);
}
@POST
@Path("msme")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save MSME (Udyam Registration) classification",
description = "Creates or updates the MSME entity classification for the given client.")
public String saveMsme(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
final JsonCommand command = toJsonCommand(clientId, apiRequestBodyAsJson);
final CommandProcessingResult result = writeService.saveMsme(clientId, command);
return resultSerializer.serialize(result);
}
private JsonCommand toJsonCommand(final Long clientId, final String json) {
final JsonElement parsedCommand = this.fromJsonHelper.parse(json);
return new JsonCommand(null, json, parsedCommand, this.fromJsonHelper,
"ENTITYCLASSIFICATION", clientId, null, null, clientId, null, null, null, null, null, null, null, null, null);
}
}
@@ -0,0 +1,41 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.entityclassification.data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Wrapper DTO containing all 7 entity classification categories for a client.
*/
@Getter
@Setter
@NoArgsConstructor
public class EntityClassificationData {
private Long clientId;
private StartupIndiaData startupIndia;
private MudraBorrowerData mudraBorrower;
private EouData eou;
private ImportExportData importExport;
private PrioritySectorData prioritySector;
private NonPrioritySectorData nonPrioritySector;
private MsmeData msme;
}
@@ -0,0 +1,47 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.entityclassification.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class EouData {
private Long id;
private Long clientId;
private Boolean enabled;
private String eouRegistrationNumber;
private String registeredDcZone;
private String bondLutNumber;
private LocalDate validityFrom;
private LocalDate validityTo;
private String gstinEou;
private BigDecimal annualExportObligation;
private BigDecimal nfeTarget;
private String softexFilingStatus;
private String brcSubmissionStatus;
private String adCategoryBankCode;
private LocalDate femaDeclarationDate;
}
@@ -0,0 +1,46 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.entityclassification.data;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class ImportExportData {
private Long id;
private Long clientId;
private Boolean enabled;
private String iecCode;
private LocalDate iecIssueDate;
private String iecStatus;
private String tradeType;
private String primaryHsCode;
private String keyTradingCountries;
private String adCategoryBankCode;
private String ecgcPolicyNumber;
private LocalDate ecgcPolicyExpiry;
private Boolean eefcAccountRequired;
private Boolean pcfcFacilityRequired;
private Boolean femaDeclarationObtained;
}
@@ -0,0 +1,49 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.entityclassification.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class MsmeData {
private Long id;
private Long clientId;
private Boolean enabled;
private String udyamRegistrationNumber;
private LocalDate registrationDate;
private String registrationStatus;
private String msmeClassification;
private String enterpriseType;
private String nicActivityCode;
private BigDecimal annualTurnover;
private BigDecimal investmentPlantMachinery;
private String financialYearAssessment;
private String socialCategoryOwner;
private String genderPrimaryOwner;
private Integer noOfEmployees;
private Boolean womenLedEnterprise;
private Boolean divyangPwdOwned;
}
@@ -0,0 +1,45 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* 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.entityclassification.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class MudraBorrowerData {
private Long id;
private Long clientId;
private Boolean enabled;
private String mudraCategory;
private String mudraLoanAccountNo;
private String lendingInstitutionName;
private BigDecimal sanctionedAmount;
private LocalDate disbursementDate;
private String loanPurpose;
private String mudraCardNumber;
private BigDecimal currentOutstanding;
private String repaymentStatus;
private Boolean npaFlag;
}
@@ -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.client.entityclassification.data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class NonPrioritySectorData {
private Long id;
private Long clientId;
private Boolean enabled;
private String industrySector;
private String nicCode;
private String industryDescription;
private String annualRevenueBand;
private String riskCategory;
private Integer customerRiskScore;
private String amlRiskClassification;
private Boolean pepFlag;
private Boolean amlWatchlistMatch;
private Boolean adverseMediaFlag;
}
@@ -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.entityclassification.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class PrioritySectorData {
private Long id;
private Long clientId;
private Boolean enabled;
private String pslCategory;
private String pslSubCategory;
private String pslLoanPurposeCode;
private String pslEligibilityBasis;
private String fiddReportingCode;
private BigDecimal pslSanctionedLimit;
private String msmeClassification;
private String udyamRegistrationNumber;
private LocalDate udyamRegistrationDate;
private BigDecimal annualTurnover;
private BigDecimal investmentPlantMachinery;
private Boolean weakerSectionFlag;
private Boolean scStBorrowerFlag;
private Boolean womenEntrepreneurFlag;
private Boolean minorityCommunityFlag;
}
@@ -0,0 +1,49 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.entityclassification.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class StartupIndiaData {
private Long id;
private Long clientId;
private Boolean enabled;
private String dpiitRecognitionNumber;
private LocalDate recognitionDate;
private LocalDate certificateExpiryDate;
private String entityStage;
private String sectorAsDpiit;
private String nicIndustryCode;
private Boolean taxExemption80iac;
private Boolean sidbiFund;
private Boolean selfCertification;
private Boolean fastTrackPatent;
private Boolean govtTenderExemption;
private Boolean creditGuaranteeCgss;
private BigDecimal pslLoanLimit;
private String pslSubCategory;
}
@@ -0,0 +1,26 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.entityclassification.service;
import org.apache.fineract.portfolio.client.entityclassification.data.EntityClassificationData;
public interface EntityClassificationReadService {
EntityClassificationData retrieveAllClassifications(Long clientId);
}
@@ -0,0 +1,218 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* 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.entityclassification.service;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.client.entityclassification.data.EntityClassificationData;
import org.apache.fineract.portfolio.client.entityclassification.data.EouData;
import org.apache.fineract.portfolio.client.entityclassification.data.ImportExportData;
import org.apache.fineract.portfolio.client.entityclassification.data.MudraBorrowerData;
import org.apache.fineract.portfolio.client.entityclassification.data.NonPrioritySectorData;
import org.apache.fineract.portfolio.client.entityclassification.data.PrioritySectorData;
import org.apache.fineract.portfolio.client.entityclassification.data.MsmeData;
import org.apache.fineract.portfolio.client.entityclassification.data.StartupIndiaData;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientEou;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientEouRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientImportExport;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientMsme;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientMsmeRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientImportExportRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientMudraBorrower;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientMudraBorrowerRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientNonPrioritySector;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientNonPrioritySectorRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientPrioritySector;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientPrioritySectorRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientStartupIndia;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientStartupIndiaRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class EntityClassificationReadServiceImpl implements EntityClassificationReadService {
private final ClientStartupIndiaRepository startupIndiaRepository;
private final ClientMudraBorrowerRepository mudraBorrowerRepository;
private final ClientEouRepository eouRepository;
private final ClientImportExportRepository importExportRepository;
private final ClientPrioritySectorRepository prioritySectorRepository;
private final ClientNonPrioritySectorRepository nonPrioritySectorRepository;
private final ClientMsmeRepository msmeRepository;
@Override
public EntityClassificationData retrieveAllClassifications(final Long clientId) {
final EntityClassificationData data = new EntityClassificationData();
data.setClientId(clientId);
startupIndiaRepository.findByClientId(clientId).ifPresent(e -> data.setStartupIndia(toStartupIndiaData(e)));
mudraBorrowerRepository.findByClientId(clientId).ifPresent(e -> data.setMudraBorrower(toMudraBorrowerData(e)));
eouRepository.findByClientId(clientId).ifPresent(e -> data.setEou(toEouData(e)));
importExportRepository.findByClientId(clientId).ifPresent(e -> data.setImportExport(toImportExportData(e)));
prioritySectorRepository.findByClientId(clientId).ifPresent(e -> data.setPrioritySector(toPrioritySectorData(e)));
nonPrioritySectorRepository.findByClientId(clientId).ifPresent(e -> data.setNonPrioritySector(toNonPrioritySectorData(e)));
msmeRepository.findByClientId(clientId).ifPresent(e -> data.setMsme(toMsmeData(e)));
return data;
}
private StartupIndiaData toStartupIndiaData(final ClientStartupIndia e) {
final StartupIndiaData d = new StartupIndiaData();
d.setId(e.getId());
d.setClientId(e.getClient().getId());
d.setEnabled(e.getEnabled());
d.setDpiitRecognitionNumber(e.getDpiitRecognitionNumber());
d.setRecognitionDate(e.getRecognitionDate());
d.setCertificateExpiryDate(e.getCertificateExpiryDate());
d.setEntityStage(e.getEntityStage());
d.setSectorAsDpiit(e.getSectorAsDpiit());
d.setNicIndustryCode(e.getNicIndustryCode());
d.setTaxExemption80iac(e.getTaxExemption80iac());
d.setSidbiFund(e.getSidbiFund());
d.setSelfCertification(e.getSelfCertification());
d.setFastTrackPatent(e.getFastTrackPatent());
d.setGovtTenderExemption(e.getGovtTenderExemption());
d.setCreditGuaranteeCgss(e.getCreditGuaranteeCgss());
d.setPslLoanLimit(e.getPslLoanLimit());
d.setPslSubCategory(e.getPslSubCategory());
return d;
}
private MudraBorrowerData toMudraBorrowerData(final ClientMudraBorrower e) {
final MudraBorrowerData d = new MudraBorrowerData();
d.setId(e.getId());
d.setClientId(e.getClient().getId());
d.setEnabled(e.getEnabled());
d.setMudraCategory(e.getMudraCategory());
d.setMudraLoanAccountNo(e.getMudraLoanAccountNo());
d.setLendingInstitutionName(e.getLendingInstitutionName());
d.setSanctionedAmount(e.getSanctionedAmount());
d.setDisbursementDate(e.getDisbursementDate());
d.setLoanPurpose(e.getLoanPurpose());
d.setMudraCardNumber(e.getMudraCardNumber());
d.setCurrentOutstanding(e.getCurrentOutstanding());
d.setRepaymentStatus(e.getRepaymentStatus());
d.setNpaFlag(e.getNpaFlag());
return d;
}
private EouData toEouData(final ClientEou e) {
final EouData d = new EouData();
d.setId(e.getId());
d.setClientId(e.getClient().getId());
d.setEnabled(e.getEnabled());
d.setEouRegistrationNumber(e.getEouRegistrationNumber());
d.setRegisteredDcZone(e.getRegisteredDcZone());
d.setBondLutNumber(e.getBondLutNumber());
d.setValidityFrom(e.getValidityFrom());
d.setValidityTo(e.getValidityTo());
d.setGstinEou(e.getGstinEou());
d.setAnnualExportObligation(e.getAnnualExportObligation());
d.setNfeTarget(e.getNfeTarget());
d.setSoftexFilingStatus(e.getSoftexFilingStatus());
d.setBrcSubmissionStatus(e.getBrcSubmissionStatus());
d.setAdCategoryBankCode(e.getAdCategoryBankCode());
d.setFemaDeclarationDate(e.getFemaDeclarationDate());
return d;
}
private ImportExportData toImportExportData(final ClientImportExport e) {
final ImportExportData d = new ImportExportData();
d.setId(e.getId());
d.setClientId(e.getClient().getId());
d.setEnabled(e.getEnabled());
d.setIecCode(e.getIecCode());
d.setIecIssueDate(e.getIecIssueDate());
d.setIecStatus(e.getIecStatus());
d.setTradeType(e.getTradeType());
d.setPrimaryHsCode(e.getPrimaryHsCode());
d.setKeyTradingCountries(e.getKeyTradingCountries());
d.setAdCategoryBankCode(e.getAdCategoryBankCode());
d.setEcgcPolicyNumber(e.getEcgcPolicyNumber());
d.setEcgcPolicyExpiry(e.getEcgcPolicyExpiry());
d.setEefcAccountRequired(e.getEefcAccountRequired());
d.setPcfcFacilityRequired(e.getPcfcFacilityRequired());
d.setFemaDeclarationObtained(e.getFemaDeclarationObtained());
return d;
}
private PrioritySectorData toPrioritySectorData(final ClientPrioritySector e) {
final PrioritySectorData d = new PrioritySectorData();
d.setId(e.getId());
d.setClientId(e.getClient().getId());
d.setEnabled(e.getEnabled());
d.setPslCategory(e.getPslCategory());
d.setPslSubCategory(e.getPslSubCategory());
d.setPslLoanPurposeCode(e.getPslLoanPurposeCode());
d.setPslEligibilityBasis(e.getPslEligibilityBasis());
d.setFiddReportingCode(e.getFiddReportingCode());
d.setPslSanctionedLimit(e.getPslSanctionedLimit());
d.setMsmeClassification(e.getMsmeClassification());
d.setUdyamRegistrationNumber(e.getUdyamRegistrationNumber());
d.setUdyamRegistrationDate(e.getUdyamRegistrationDate());
d.setAnnualTurnover(e.getAnnualTurnover());
d.setInvestmentPlantMachinery(e.getInvestmentPlantMachinery());
d.setWeakerSectionFlag(e.getWeakerSectionFlag());
d.setScStBorrowerFlag(e.getScStBorrowerFlag());
d.setWomenEntrepreneurFlag(e.getWomenEntrepreneurFlag());
d.setMinorityCommunityFlag(e.getMinorityCommunityFlag());
return d;
}
private MsmeData toMsmeData(final ClientMsme e) {
final MsmeData d = new MsmeData();
d.setId(e.getId());
d.setClientId(e.getClient().getId());
d.setEnabled(e.getEnabled());
d.setUdyamRegistrationNumber(e.getUdyamRegistrationNumber());
d.setRegistrationDate(e.getRegistrationDate());
d.setRegistrationStatus(e.getRegistrationStatus());
d.setMsmeClassification(e.getMsmeClassification());
d.setEnterpriseType(e.getEnterpriseType());
d.setNicActivityCode(e.getNicActivityCode());
d.setAnnualTurnover(e.getAnnualTurnover());
d.setInvestmentPlantMachinery(e.getInvestmentPlantMachinery());
d.setFinancialYearAssessment(e.getFinancialYearAssessment());
d.setSocialCategoryOwner(e.getSocialCategoryOwner());
d.setGenderPrimaryOwner(e.getGenderPrimaryOwner());
d.setNoOfEmployees(e.getNoOfEmployees());
d.setWomenLedEnterprise(e.getWomenLedEnterprise());
d.setDivyangPwdOwned(e.getDivyangPwdOwned());
return d;
}
private NonPrioritySectorData toNonPrioritySectorData(final ClientNonPrioritySector e) {
final NonPrioritySectorData d = new NonPrioritySectorData();
d.setId(e.getId());
d.setClientId(e.getClient().getId());
d.setEnabled(e.getEnabled());
d.setIndustrySector(e.getIndustrySector());
d.setNicCode(e.getNicCode());
d.setIndustryDescription(e.getIndustryDescription());
d.setAnnualRevenueBand(e.getAnnualRevenueBand());
d.setRiskCategory(e.getRiskCategory());
d.setCustomerRiskScore(e.getCustomerRiskScore());
d.setAmlRiskClassification(e.getAmlRiskClassification());
d.setPepFlag(e.getPepFlag());
d.setAmlWatchlistMatch(e.getAmlWatchlistMatch());
d.setAdverseMediaFlag(e.getAdverseMediaFlag());
return d;
}
}
@@ -0,0 +1,39 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* 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.entityclassification.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface EntityClassificationWriteService {
CommandProcessingResult saveStartupIndia(Long clientId, JsonCommand command);
CommandProcessingResult saveMudraBorrower(Long clientId, JsonCommand command);
CommandProcessingResult saveEou(Long clientId, JsonCommand command);
CommandProcessingResult saveImportExport(Long clientId, JsonCommand command);
CommandProcessingResult savePrioritySector(Long clientId, JsonCommand command);
CommandProcessingResult saveNonPrioritySector(Long clientId, JsonCommand command);
CommandProcessingResult saveMsme(Long clientId, JsonCommand command);
}
@@ -0,0 +1,338 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* 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.entityclassification.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientEou;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientEouRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientImportExport;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientMsme;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientMsmeRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientImportExportRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientMudraBorrower;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientMudraBorrowerRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientNonPrioritySector;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientNonPrioritySectorRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientPrioritySector;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientPrioritySectorRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientStartupIndia;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientStartupIndiaRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class EntityClassificationWriteServiceImpl implements EntityClassificationWriteService {
private final PlatformSecurityContext context;
private final ClientRepositoryWrapper clientRepository;
private final FromJsonHelper fromJsonHelper;
private final ClientStartupIndiaRepository startupIndiaRepository;
private final ClientMudraBorrowerRepository mudraBorrowerRepository;
private final ClientEouRepository eouRepository;
private final ClientImportExportRepository importExportRepository;
private final ClientPrioritySectorRepository prioritySectorRepository;
private final ClientNonPrioritySectorRepository nonPrioritySectorRepository;
private final ClientMsmeRepository msmeRepository;
@Transactional
@Override
public CommandProcessingResult saveStartupIndia(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
final ClientStartupIndia entity = startupIndiaRepository.findByClientId(clientId).orElseGet(() -> {
final ClientStartupIndia newEntity = new ClientStartupIndia();
newEntity.setClient(client);
return newEntity;
});
entity.setEnabled(boolVal(command, "enabled"));
entity.setDpiitRecognitionNumber(strVal(command, "dpiitRecognitionNumber"));
entity.setRecognitionDate(dateVal(command, "recognitionDate"));
entity.setCertificateExpiryDate(dateVal(command, "certificateExpiryDate"));
entity.setEntityStage(strVal(command, "entityStage"));
entity.setSectorAsDpiit(strVal(command, "sectorAsDpiit"));
entity.setNicIndustryCode(strVal(command, "nicIndustryCode"));
entity.setTaxExemption80iac(boolVal(command, "taxExemption80iac"));
entity.setSidbiFund(boolVal(command, "sidbiFund"));
entity.setSelfCertification(boolVal(command, "selfCertification"));
entity.setFastTrackPatent(boolVal(command, "fastTrackPatent"));
entity.setGovtTenderExemption(boolVal(command, "govtTenderExemption"));
entity.setCreditGuaranteeCgss(boolVal(command, "creditGuaranteeCgss"));
entity.setPslLoanLimit(decVal(command, "pslLoanLimit"));
entity.setPslSubCategory(strVal(command, "pslSubCategory"));
startupIndiaRepository.saveAndFlush(entity);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(entity.getId())
.build();
}
@Transactional
@Override
public CommandProcessingResult saveMudraBorrower(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
final ClientMudraBorrower entity = mudraBorrowerRepository.findByClientId(clientId).orElseGet(() -> {
final ClientMudraBorrower newEntity = new ClientMudraBorrower();
newEntity.setClient(client);
return newEntity;
});
entity.setEnabled(boolVal(command, "enabled"));
entity.setMudraCategory(strVal(command, "mudraCategory"));
entity.setMudraLoanAccountNo(strVal(command, "mudraLoanAccountNo"));
entity.setLendingInstitutionName(strVal(command, "lendingInstitutionName"));
entity.setSanctionedAmount(decVal(command, "sanctionedAmount"));
entity.setDisbursementDate(dateVal(command, "disbursementDate"));
entity.setLoanPurpose(strVal(command, "loanPurpose"));
entity.setMudraCardNumber(strVal(command, "mudraCardNumber"));
entity.setCurrentOutstanding(decVal(command, "currentOutstanding"));
entity.setRepaymentStatus(strVal(command, "repaymentStatus"));
entity.setNpaFlag(boolVal(command, "npaFlag"));
mudraBorrowerRepository.saveAndFlush(entity);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(entity.getId())
.build();
}
@Transactional
@Override
public CommandProcessingResult saveEou(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
final ClientEou entity = eouRepository.findByClientId(clientId).orElseGet(() -> {
final ClientEou newEntity = new ClientEou();
newEntity.setClient(client);
return newEntity;
});
entity.setEnabled(boolVal(command, "enabled"));
entity.setEouRegistrationNumber(strVal(command, "eouRegistrationNumber"));
entity.setRegisteredDcZone(strVal(command, "registeredDcZone"));
entity.setBondLutNumber(strVal(command, "bondLutNumber"));
entity.setValidityFrom(dateVal(command, "validityFrom"));
entity.setValidityTo(dateVal(command, "validityTo"));
entity.setGstinEou(strVal(command, "gstinEou"));
entity.setAnnualExportObligation(decVal(command, "annualExportObligation"));
entity.setNfeTarget(decVal(command, "nfeTarget"));
entity.setSoftexFilingStatus(strVal(command, "softexFilingStatus"));
entity.setBrcSubmissionStatus(strVal(command, "brcSubmissionStatus"));
entity.setAdCategoryBankCode(strVal(command, "adCategoryBankCode"));
entity.setFemaDeclarationDate(dateVal(command, "femaDeclarationDate"));
eouRepository.saveAndFlush(entity);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(entity.getId())
.build();
}
@Transactional
@Override
public CommandProcessingResult saveImportExport(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
final ClientImportExport entity = importExportRepository.findByClientId(clientId).orElseGet(() -> {
final ClientImportExport newEntity = new ClientImportExport();
newEntity.setClient(client);
return newEntity;
});
entity.setEnabled(boolVal(command, "enabled"));
entity.setIecCode(strVal(command, "iecCode"));
entity.setIecIssueDate(dateVal(command, "iecIssueDate"));
entity.setIecStatus(strVal(command, "iecStatus"));
entity.setTradeType(strVal(command, "tradeType"));
entity.setPrimaryHsCode(strVal(command, "primaryHsCode"));
entity.setKeyTradingCountries(strVal(command, "keyTradingCountries"));
entity.setAdCategoryBankCode(strVal(command, "adCategoryBankCode"));
entity.setEcgcPolicyNumber(strVal(command, "ecgcPolicyNumber"));
entity.setEcgcPolicyExpiry(dateVal(command, "ecgcPolicyExpiry"));
entity.setEefcAccountRequired(boolVal(command, "eefcAccountRequired"));
entity.setPcfcFacilityRequired(boolVal(command, "pcfcFacilityRequired"));
entity.setFemaDeclarationObtained(boolVal(command, "femaDeclarationObtained"));
importExportRepository.saveAndFlush(entity);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(entity.getId())
.build();
}
@Transactional
@Override
public CommandProcessingResult savePrioritySector(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
final ClientPrioritySector entity = prioritySectorRepository.findByClientId(clientId).orElseGet(() -> {
final ClientPrioritySector newEntity = new ClientPrioritySector();
newEntity.setClient(client);
return newEntity;
});
entity.setEnabled(boolVal(command, "enabled"));
entity.setPslCategory(strVal(command, "pslCategory"));
entity.setPslSubCategory(strVal(command, "pslSubCategory"));
entity.setPslLoanPurposeCode(strVal(command, "pslLoanPurposeCode"));
entity.setPslEligibilityBasis(strVal(command, "pslEligibilityBasis"));
entity.setFiddReportingCode(strVal(command, "fiddReportingCode"));
entity.setPslSanctionedLimit(decVal(command, "pslSanctionedLimit"));
entity.setMsmeClassification(strVal(command, "msmeClassification"));
entity.setUdyamRegistrationNumber(strVal(command, "udyamRegistrationNumber"));
entity.setUdyamRegistrationDate(dateVal(command, "udyamRegistrationDate"));
entity.setAnnualTurnover(decVal(command, "annualTurnover"));
entity.setInvestmentPlantMachinery(decVal(command, "investmentPlantMachinery"));
entity.setWeakerSectionFlag(boolVal(command, "weakerSectionFlag"));
entity.setScStBorrowerFlag(boolVal(command, "scStBorrowerFlag"));
entity.setWomenEntrepreneurFlag(boolVal(command, "womenEntrepreneurFlag"));
entity.setMinorityCommunityFlag(boolVal(command, "minorityCommunityFlag"));
prioritySectorRepository.saveAndFlush(entity);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(entity.getId())
.build();
}
@Transactional
@Override
public CommandProcessingResult saveNonPrioritySector(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
final ClientNonPrioritySector entity = nonPrioritySectorRepository.findByClientId(clientId).orElseGet(() -> {
final ClientNonPrioritySector newEntity = new ClientNonPrioritySector();
newEntity.setClient(client);
return newEntity;
});
entity.setEnabled(boolVal(command, "enabled"));
entity.setIndustrySector(strVal(command, "industrySector"));
entity.setNicCode(strVal(command, "nicCode"));
entity.setIndustryDescription(strVal(command, "industryDescription"));
entity.setAnnualRevenueBand(strVal(command, "annualRevenueBand"));
entity.setRiskCategory(strVal(command, "riskCategory"));
entity.setCustomerRiskScore(intVal(command, "customerRiskScore"));
entity.setAmlRiskClassification(strVal(command, "amlRiskClassification"));
entity.setPepFlag(boolVal(command, "pepFlag"));
entity.setAmlWatchlistMatch(boolVal(command, "amlWatchlistMatch"));
entity.setAdverseMediaFlag(boolVal(command, "adverseMediaFlag"));
nonPrioritySectorRepository.saveAndFlush(entity);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(entity.getId())
.build();
}
@Transactional
@Override
public CommandProcessingResult saveMsme(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
final ClientMsme entity = msmeRepository.findByClientId(clientId).orElseGet(() -> {
final ClientMsme newEntity = new ClientMsme();
newEntity.setClient(client);
return newEntity;
});
entity.setEnabled(boolVal(command, "enabled"));
entity.setUdyamRegistrationNumber(strVal(command, "udyamRegistrationNumber"));
entity.setRegistrationDate(dateVal(command, "registrationDate"));
entity.setRegistrationStatus(strVal(command, "registrationStatus"));
entity.setMsmeClassification(strVal(command, "msmeClassification"));
entity.setEnterpriseType(strVal(command, "enterpriseType"));
entity.setNicActivityCode(strVal(command, "nicActivityCode"));
entity.setAnnualTurnover(decVal(command, "annualTurnover"));
entity.setInvestmentPlantMachinery(decVal(command, "investmentPlantMachinery"));
entity.setFinancialYearAssessment(strVal(command, "financialYearAssessment"));
entity.setSocialCategoryOwner(strVal(command, "socialCategoryOwner"));
entity.setGenderPrimaryOwner(strVal(command, "genderPrimaryOwner"));
entity.setNoOfEmployees(intVal(command, "noOfEmployees"));
entity.setWomenLedEnterprise(boolVal(command, "womenLedEnterprise"));
entity.setDivyangPwdOwned(boolVal(command, "divyangPwdOwned"));
msmeRepository.saveAndFlush(entity);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(entity.getId())
.build();
}
// --- Helper methods for extracting values from JsonCommand ---
private String strVal(final JsonCommand command, final String paramName) {
return command.stringValueOfParameterNamed(paramName);
}
private Boolean boolVal(final JsonCommand command, final String paramName) {
if (command.parameterExists(paramName)) {
return command.booleanObjectValueOfParameterNamed(paramName);
}
return null;
}
private LocalDate dateVal(final JsonCommand command, final String paramName) {
return command.localDateValueOfParameterNamed(paramName);
}
private BigDecimal decVal(final JsonCommand command, final String paramName) {
return command.bigDecimalValueOfParameterNamed(paramName);
}
private Integer intVal(final JsonCommand command, final String paramName) {
if (command.parameterExists(paramName)) {
return command.integerValueOfParameterNamed(paramName);
}
return null;
}
}
@@ -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.ClientForm60WritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CLIENTFORM60", action = "CREATE")
@RequiredArgsConstructor
public class CreateClientForm60CommandHandler implements NewCommandSourceHandler {
private final ClientForm60WritePlatformService form60WritePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return form60WritePlatformService.createForm60(command.getClientId(), command);
}
}
@@ -0,0 +1,49 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.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-cin} KYC command.
*
* <p>Calls LincPay {@code /validate-company} with the CIN stored on the KYC record.
* On success, marks {@code cinVerified = true} on the KYC record.
*/
@Service
@CommandType(entity = "CLIENTKYC", action = "VERIFYCIN")
@RequiredArgsConstructor
public class VerifyCinClientKycCommandHandler implements NewCommandSourceHandler {
private final ClientKycWritePlatformService clientKycWritePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return clientKycWritePlatformService.verifyCin(
command.getClientId(), command.entityId(), command);
}
}
@@ -0,0 +1,49 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.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-din} KYC command.
*
* <p>Calls LincPay {@code /validate-director} with the DIN stored on the KYC record.
* On success, marks {@code dinVerified = true} on the KYC record.
*/
@Service
@CommandType(entity = "CLIENTKYC", action = "VERIFYDIN")
@RequiredArgsConstructor
public class VerifyDinClientKycCommandHandler implements NewCommandSourceHandler {
private final ClientKycWritePlatformService clientKycWritePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return clientKycWritePlatformService.verifyDin(
command.getClientId(), command.entityId(), command);
}
}
@@ -74,4 +74,20 @@ public interface KycVerificationService {
* @return verification result
*/
KycVerificationResult verifyGstin(String gstinNumber);
/**
* Verify a CIN (Corporate Identification Number) via the MCA database through LincPay API.
*
* @param cinNumber 21-character CIN (e.g. {@code U12345MH2020PTC123456})
* @return verification result containing company master data
*/
KycVerificationResult verifyCin(String cinNumber);
/**
* Verify a DIN (Director Identification Number) via the MCA database through LincPay API.
*
* @param dinNumber 8-digit DIN
* @return verification result containing director and associated company data
*/
KycVerificationResult verifyDin(String dinNumber);
}
@@ -67,4 +67,18 @@ public class KycVerificationServiceImpl implements KycVerificationService {
return KycVerificationResult.success("STUB-GSTIN-REF-" + gstinNumber,
"{\"status\":\"SUCCESS\",\"message\":\"Stub: GSTIN verified\",\"gstin\":\"" + gstinNumber + "\"}");
}
@Override
public KycVerificationResult verifyCin(final String cinNumber) {
LOG.info("[KYC-STUB] CIN verification called for CIN: {}. Replace with LincPay.", cinNumber);
return KycVerificationResult.success("STUB-CIN-REF-" + cinNumber,
"{\"status\":\"SUCCESS\",\"message\":\"Stub: CIN verified\",\"cin\":\"" + cinNumber + "\"}");
}
@Override
public KycVerificationResult verifyDin(final String dinNumber) {
LOG.info("[KYC-STUB] DIN verification called for DIN: {}. Replace with LincPay.", dinNumber);
return KycVerificationResult.success("STUB-DIN-REF-" + dinNumber,
"{\"status\":\"SUCCESS\",\"message\":\"Stub: DIN verified\",\"din\":\"" + dinNumber + "\"}");
}
}
@@ -90,6 +90,12 @@ public class LincPayKycVerificationServiceImpl implements KycVerificationService
@Value("${fineract.kyc.lincpay.path.gstin-validate}")
private String gstinValidatePath;
@Value("${fineract.kyc.lincpay.path.cin-validate}")
private String cinValidatePath;
@Value("${fineract.kyc.lincpay.path.din-validate}")
private String dinValidatePath;
private final RestTemplate restTemplate;
public LincPayKycVerificationServiceImpl() {
@@ -274,6 +280,88 @@ public class LincPayKycVerificationServiceImpl implements KycVerificationService
}
}
// ── CIN Validation (Company Identification Number) ─────────────────────
/**
* POST https://ekyc.lincpay.in/EKYC/ekyc/validate-company
* Body: { "apiKey": "...", "cin": "...", "secretKey": "..." }
*/
@Override
public KycVerificationResult verifyCin(final String cinNumber) {
final String url = baseUrl + cinValidatePath;
try {
final JsonObject requestBody = new JsonObject();
requestBody.addProperty("apiKey", apiKey);
requestBody.addProperty("cin", cinNumber);
requestBody.addProperty("secretKey", secret);
LOG.info("LincPay CIN request URL: {}", url);
final String rawResponse = post(url, requestBody.toString());
LOG.info("LincPay CIN raw response: {}", rawResponse);
final JsonObject response = parseJson(rawResponse);
if (isSuccess(response)) {
LOG.info("LincPay CIN verified successfully for CIN: {}", cinNumber);
return KycVerificationResult.success("CIN-" + cinNumber, rawResponse);
}
final String errorMsg = extractMessage(response);
LOG.warn("LincPay CIN validation failed for CIN {}: {}", cinNumber, errorMsg);
return KycVerificationResult.failure(errorMsg, rawResponse);
} catch (final HttpClientErrorException | HttpServerErrorException e) {
LOG.error("LincPay CIN validation HTTP error for CIN {}: {} {}", cinNumber,
e.getStatusCode(), e.getResponseBodyAsString());
return KycVerificationResult.failure(
"CIN verification failed (HTTP " + e.getStatusCode() + "): "
+ e.getResponseBodyAsString(), null);
} catch (final Exception e) {
LOG.error("LincPay CIN validation unexpected error for CIN: {}", cinNumber, e);
return KycVerificationResult.failure("CIN verification failed: " + e.getMessage(), null);
}
}
// ── DIN Validation (Director Identification Number) ──────────────────────
/**
* POST https://ekyc.lincpay.in/EKYC/ekyc/validate-director
* Body: { "apiKey": "...", "din": "...", "secretKey": "..." }
*/
@Override
public KycVerificationResult verifyDin(final String dinNumber) {
final String url = baseUrl + dinValidatePath;
try {
final JsonObject requestBody = new JsonObject();
requestBody.addProperty("apiKey", apiKey);
requestBody.addProperty("din", dinNumber);
requestBody.addProperty("secretKey", secret);
LOG.info("LincPay DIN request URL: {}", url);
final String rawResponse = post(url, requestBody.toString());
LOG.info("LincPay DIN raw response: {}", rawResponse);
final JsonObject response = parseJson(rawResponse);
if (isSuccess(response)) {
LOG.info("LincPay DIN verified successfully for DIN: {}", dinNumber);
return KycVerificationResult.success("DIN-" + dinNumber, rawResponse);
}
final String errorMsg = extractMessage(response);
LOG.warn("LincPay DIN validation failed for DIN {}: {}", dinNumber, errorMsg);
return KycVerificationResult.failure(errorMsg, rawResponse);
} catch (final HttpClientErrorException | HttpServerErrorException e) {
LOG.error("LincPay DIN validation HTTP error for DIN {}: {} {}", dinNumber,
e.getStatusCode(), e.getResponseBodyAsString());
return KycVerificationResult.failure(
"DIN verification failed (HTTP " + e.getStatusCode() + "): "
+ e.getResponseBodyAsString(), null);
} catch (final Exception e) {
LOG.error("LincPay DIN validation unexpected error for DIN: {}", dinNumber, e);
return KycVerificationResult.failure("DIN verification failed: " + e.getMessage(), null);
}
}
// ── Internal helpers ──────────────────────────────────────────────────────
private String post(final String url, final String requestBodyJson) {
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.portfolio.client.mapper;
import java.time.LocalDate;
import java.time.Period;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
@@ -27,8 +29,10 @@ import org.apache.fineract.portfolio.client.data.ClientTimelineData;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientEnumerations;
import org.apache.fineract.useradministration.domain.AppUser;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.Named;
@Mapper(config = MapstructMapperConfig.class)
@@ -43,6 +47,8 @@ public interface ClientMapper {
@Mapping(target = "transferToOfficeName", source = "transferToOffice.name")
@Mapping(target = "externalId", source = "externalId")
@Mapping(target = "gender", source = "source", qualifiedByName = "clientGenderCode")
@Mapping(target = "salutation", source = "source", qualifiedByName = "salutationCode")
@Mapping(target = "salutationOptions", ignore = true)
@Mapping(target = "imageId", source = "imageId")
@Mapping(target = "staffId", source = "staff.id")
@Mapping(target = "staffName", source = "staff.displayName")
@@ -54,10 +60,16 @@ public interface ClientMapper {
@Mapping(target = "clientClassification", source = "source", qualifiedByName = "clientClassificationCode")
@Mapping(target = "occupationCategory", source = "source", qualifiedByName = "occupationCategoryCode")
@Mapping(target = "occupation", source = "source", qualifiedByName = "occupationCode")
@Mapping(target = "maritalStatus", source = "source", qualifiedByName = "maritalStatusCode")
@Mapping(target = "occupationCategoryOptions", ignore = true)
@Mapping(target = "occupationOptions", ignore = true)
@Mapping(target = "legalForm", source = "source", qualifiedByName = "clientLegalFormEnum")
@Mapping(target = "isStaff", source = "source", qualifiedByName = "clientIsStaff")
@Mapping(target = "ageYears", ignore = true)
@Mapping(target = "ageMonths", ignore = true)
@Mapping(target = "ageDays", ignore = true)
@Mapping(target = "guardianClientId", source = "source", qualifiedByName = "guardianClientIdValue")
@Mapping(target = "guardianClientDisplayName", source = "source", qualifiedByName = "guardianClientDisplayNameValue")
@Mapping(target = "imagePresent", ignore = true)
@Mapping(target = "officeOptions", ignore = true)
@Mapping(target = "staffOptions", ignore = true)
@@ -122,6 +134,24 @@ public interface ClientMapper {
return CodeValueData.instance(code.getId(), code.getLabel());
}
@Named("salutationCode")
default CodeValueData salutationCode(Client client) {
final CodeValue code = client.getSalutation();
if (code == null) {
return null;
}
return CodeValueData.instance(code.getId(), code.getLabel());
}
@Named("maritalStatusCode")
default CodeValueData maritalStatusCode(Client client) {
final CodeValue code = client.getMaritalStatus();
if (code == null) {
return null;
}
return CodeValueData.instance(code.getId(), code.getLabel());
}
@Named("clientSubStatusCode")
default CodeValueData clientSubStatusCode(Client client) {
final CodeValue code = client.getSubStatus();
@@ -178,4 +208,25 @@ public interface ClientMapper {
return Boolean.valueOf(client.isStaff());
}
@Named("guardianClientIdValue")
default Long guardianClientIdValue(Client client) {
return client.getGuardianClient() != null ? client.getGuardianClient().getId() : null;
}
@Named("guardianClientDisplayNameValue")
default String guardianClientDisplayNameValue(Client client) {
return client.getGuardianClient() != null ? client.getGuardianClient().getDisplayName() : null;
}
@AfterMapping
default void computeAge(Client source, @MappingTarget ClientData target) {
final LocalDate dob = source.getDateOfBirth();
if (dob != null) {
final Period age = Period.between(dob, LocalDate.now());
target.setAgeYears(age.getYears());
target.setAgeMonths(age.getMonths());
target.setAgeDays(age.getDays());
}
}
}
@@ -62,10 +62,25 @@ public final class ClientFamilyMemberCommandFromApiJsonDeserializer {
public static final String NOMINATION_TYPE = "nominationType";
public static final String PERCENTAGE_SHARE = "percentageShare";
public static final String REMARKS = "remarks";
public static final String AADHAAR_NUMBER = "aadhaarNumber";
public static final String PAN_NUMBER = "panNumber";
public static final String NATIONALITY = "nationality";
public static final String IS_ALIVE = "isAlive";
public static final String LINKED_CLIENT_ID = "linkedClientId";
public static final String MARRIAGE_DATE = "marriageDate";
public static final String MARRIAGE_AUTHORITY = "marriageAuthority";
private static final java.util.regex.Pattern AADHAAR_PATTERN = java.util.regex.Pattern.compile("^\\d{12}$");
private static final java.util.regex.Pattern PAN_PATTERN = java.util.regex.Pattern.compile("^[A-Z]{5}[0-9]{4}[A-Z]$");
private static final Set<String> VALID_MARRIAGE_AUTHORITIES = Set.of(
"SUB_REGISTRAR_OFFICE", "MARRIAGE_REGISTRAR", "COURT_MARRIAGE_OFFICER",
"MUNICIPAL_AUTHORITY", "EMBASSY_CONSULATE", "DEFENCE_AUTHORITY");
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,
RELATIONSHIP_ID, MARITAL_STATUS_ID, GENDER_ID, DATE_OF_BIRTH, PROFESSION_ID, LOCALE, DATE_FORMAT, FAMILY_MEMBERS,
NOMINATION_TYPE, PERCENTAGE_SHARE, REMARKS));
NOMINATION_TYPE, PERCENTAGE_SHARE, REMARKS,
AADHAAR_NUMBER, PAN_NUMBER, NATIONALITY, IS_ALIVE, LINKED_CLIENT_ID, MARRIAGE_DATE, MARRIAGE_AUTHORITY));
public static final String FAMILY_MEMBERS1 = "FamilyMembers";
public static final String RELATION_SHIP_ID = "relationShipId";
private final FromJsonHelper fromApiJsonHelper;
@@ -207,6 +222,44 @@ public final class ClientFamilyMemberCommandFromApiJsonDeserializer {
baseDataValidator.reset().parameter(REMARKS).value(remarks).notExceedingLengthOf(500);
}
// CBS extended field validations
if (this.fromApiJsonHelper.extractStringNamed(AADHAAR_NUMBER, element) != null) {
final String aadhaar = this.fromApiJsonHelper.extractStringNamed(AADHAAR_NUMBER, element);
baseDataValidator.reset().parameter(AADHAAR_NUMBER).value(aadhaar).notExceedingLengthOf(12);
if (aadhaar != null && !aadhaar.isEmpty() && !AADHAAR_PATTERN.matcher(aadhaar).matches()) {
baseDataValidator.reset().parameter(AADHAAR_NUMBER).value(aadhaar)
.failWithCode("invalid.format", "Aadhaar number must be exactly 12 digits");
}
}
if (this.fromApiJsonHelper.extractStringNamed(PAN_NUMBER, element) != null) {
final String pan = this.fromApiJsonHelper.extractStringNamed(PAN_NUMBER, element);
baseDataValidator.reset().parameter(PAN_NUMBER).value(pan).notExceedingLengthOf(10);
if (pan != null && !pan.isEmpty() && !PAN_PATTERN.matcher(pan).matches()) {
baseDataValidator.reset().parameter(PAN_NUMBER).value(pan)
.failWithCode("invalid.format", "PAN must match format ABCDE1234F");
}
}
if (this.fromApiJsonHelper.extractStringNamed(NATIONALITY, element) != null) {
final String nationality = this.fromApiJsonHelper.extractStringNamed(NATIONALITY, element);
baseDataValidator.reset().parameter(NATIONALITY).value(nationality).notExceedingLengthOf(50);
}
if (this.fromApiJsonHelper.extractStringNamed(MARRIAGE_AUTHORITY, element) != null) {
final String authority = this.fromApiJsonHelper.extractStringNamed(MARRIAGE_AUTHORITY, element);
baseDataValidator.reset().parameter(MARRIAGE_AUTHORITY).value(authority).notExceedingLengthOf(50);
if (authority != null && !authority.isEmpty() && !VALID_MARRIAGE_AUTHORITIES.contains(authority)) {
baseDataValidator.reset().parameter(MARRIAGE_AUTHORITY).value(authority)
.failWithCode("invalid.value", "Must be one of: " + VALID_MARRIAGE_AUTHORITIES);
}
}
if (this.fromApiJsonHelper.extractLongNamed(LINKED_CLIENT_ID, element) != null) {
final long linkedId = this.fromApiJsonHelper.extractLongNamed(LINKED_CLIENT_ID, element);
baseDataValidator.reset().parameter(LINKED_CLIENT_ID).value(linkedId).longGreaterThanZero();
}
}
public void validateForUpdate(final long familyMemberId, String json) {
@@ -292,6 +345,44 @@ public final class ClientFamilyMemberCommandFromApiJsonDeserializer {
baseDataValidator.reset().parameter(REMARKS).value(remarks).notExceedingLengthOf(500);
}
// CBS extended field validations
if (this.fromApiJsonHelper.extractStringNamed(AADHAAR_NUMBER, element) != null) {
final String aadhaar = this.fromApiJsonHelper.extractStringNamed(AADHAAR_NUMBER, element);
baseDataValidator.reset().parameter(AADHAAR_NUMBER).value(aadhaar).notExceedingLengthOf(12);
if (aadhaar != null && !aadhaar.isEmpty() && !AADHAAR_PATTERN.matcher(aadhaar).matches()) {
baseDataValidator.reset().parameter(AADHAAR_NUMBER).value(aadhaar)
.failWithCode("invalid.format", "Aadhaar number must be exactly 12 digits");
}
}
if (this.fromApiJsonHelper.extractStringNamed(PAN_NUMBER, element) != null) {
final String pan = this.fromApiJsonHelper.extractStringNamed(PAN_NUMBER, element);
baseDataValidator.reset().parameter(PAN_NUMBER).value(pan).notExceedingLengthOf(10);
if (pan != null && !pan.isEmpty() && !PAN_PATTERN.matcher(pan).matches()) {
baseDataValidator.reset().parameter(PAN_NUMBER).value(pan)
.failWithCode("invalid.format", "PAN must match format ABCDE1234F");
}
}
if (this.fromApiJsonHelper.extractStringNamed(NATIONALITY, element) != null) {
final String nationality = this.fromApiJsonHelper.extractStringNamed(NATIONALITY, element);
baseDataValidator.reset().parameter(NATIONALITY).value(nationality).notExceedingLengthOf(50);
}
if (this.fromApiJsonHelper.extractStringNamed(MARRIAGE_AUTHORITY, element) != null) {
final String authority = this.fromApiJsonHelper.extractStringNamed(MARRIAGE_AUTHORITY, element);
baseDataValidator.reset().parameter(MARRIAGE_AUTHORITY).value(authority).notExceedingLengthOf(50);
if (authority != null && !authority.isEmpty() && !VALID_MARRIAGE_AUTHORITIES.contains(authority)) {
baseDataValidator.reset().parameter(MARRIAGE_AUTHORITY).value(authority)
.failWithCode("invalid.value", "Must be one of: " + VALID_MARRIAGE_AUTHORITIES);
}
}
if (this.fromApiJsonHelper.extractLongNamed(LINKED_CLIENT_ID, element) != null) {
final long linkedId = this.fromApiJsonHelper.extractLongNamed(LINKED_CLIENT_ID, element);
baseDataValidator.reset().parameter(LINKED_CLIENT_ID).value(linkedId).longGreaterThanZero();
}
}
public void validateForDelete(final long familyMemberId) {
@@ -57,10 +57,15 @@ public class ClientFamilyMembersReadPlatformServiceImpl implements ClientFamilyM
+ "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,"
+ "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"
+ "fmb.nomination_type AS nominationType, fmb.percentage_share AS percentageShare, fmb.remarks AS remarks,"
+ "fmb.aadhaar_number AS aadhaarNumber, fmb.pan_number AS panNumber, fmb.nationality AS nationality,"
+ "fmb.is_alive AS isAlive, fmb.linked_client_id AS linkedClientId, fmb.marriage_date AS marriageDate,"
+ "fmb.marriage_authority AS marriageAuthority,"
+ "lc.display_name AS linkedClientName, lc.cif_number AS linkedClientCif"
+ " 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 c2 ON fmb.profession_cv_id=c2.id";
+ " LEFT JOIN m_code_value c2 ON fmb.profession_cv_id=c2.id"
+ " LEFT JOIN m_client lc ON fmb.linked_client_id=lc.id";
}
@Override
@@ -86,12 +91,24 @@ public class ClientFamilyMembersReadPlatformServiceImpl implements ClientFamilyM
final String nominationType = rs.getString("nominationType");
final BigDecimal percentageShare = rs.getBigDecimal("percentageShare");
final String remarks = rs.getString("remarks");
final String aadhaarNumber = rs.getString("aadhaarNumber");
final String panNumber = rs.getString("panNumber");
final String nationality = rs.getString("nationality");
final Boolean isAlive = rs.getObject("isAlive") != null ? rs.getBoolean("isAlive") : null;
final Long linkedClientId = JdbcSupport.getLong(rs, "linkedClientId");
final String linkedClientName = rs.getString("linkedClientName");
final String linkedClientCif = rs.getString("linkedClientCif");
final LocalDate marriageDate = JdbcSupport.getLocalDate(rs, "marriageDate");
final String marriageAuthority = rs.getString("marriageAuthority");
return ClientFamilyMembersData.builder().id(id).clientId(clientId).firstName(firstName).middleName(middleName)
.lastName(lastName).qualification(qualification).mobileNumber(mobileNumber).age(age).isDependent(isDependent)
.relationship(relationship).relationshipId(relationshipId).maritalStatus(maritalStatus).maritalStatusId(maritalStatusId)
.gender(gender).genderId(genderId).dateOfBirth(dateOfBirth).profession(profession).professionId(professionId)
.nominationType(nominationType).percentageShare(percentageShare).remarks(remarks).build();
.nominationType(nominationType).percentageShare(percentageShare).remarks(remarks)
.aadhaarNumber(aadhaarNumber).panNumber(panNumber).nationality(nationality)
.isAlive(isAlive).linkedClientId(linkedClientId).linkedClientName(linkedClientName)
.linkedClientCif(linkedClientCif).marriageDate(marriageDate).marriageAuthority(marriageAuthority).build();
}
}
@@ -129,6 +129,19 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
mobileNumber, age, isDependent, relationship, maritalStatus, gender, dateOfBirth, profession,
nominationType, percentageShare, remarks);
// CBS extended fields
clientFamilyMembers.setAadhaarNumber(command.stringValueOfParameterNamed("aadhaarNumber"));
clientFamilyMembers.setPanNumber(command.stringValueOfParameterNamed("panNumber"));
clientFamilyMembers.setNationality(command.stringValueOfParameterNamed("nationality"));
final Boolean isAlive = command.booleanObjectValueOfParameterNamed("isAlive");
clientFamilyMembers.setIsAlive(isAlive != null ? isAlive : true);
clientFamilyMembers.setMarriageDate(command.localDateValueOfParameterNamed("marriageDate"));
clientFamilyMembers.setMarriageAuthority(command.stringValueOfParameterNamed("marriageAuthority"));
final Long linkedClientId = command.longValueOfParameterNamed("linkedClientId");
if (linkedClientId != null) {
clientFamilyMembers.setLinkedClient(clientRepositoryWrapper.findOneWithNotFoundDetection(linkedClientId));
}
this.clientFamilyRepository.saveAndFlush(clientFamilyMembers);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(clientFamilyMembers.getId()).build();
@@ -258,6 +271,34 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
isDependent, relationship, maritalStatus, gender, dateOfBirth, profession,
nominationType, percentageShare, remarks);
// CBS extended fields
if (member.get("aadhaarNumber") != null && !member.get("aadhaarNumber").isJsonNull()) {
familyMember.setAadhaarNumber(member.get("aadhaarNumber").getAsString());
}
if (member.get("panNumber") != null && !member.get("panNumber").isJsonNull()) {
familyMember.setPanNumber(member.get("panNumber").getAsString());
}
if (member.get("nationality") != null && !member.get("nationality").isJsonNull()) {
familyMember.setNationality(member.get("nationality").getAsString());
}
if (member.get("isAlive") != null && !member.get("isAlive").isJsonNull()) {
familyMember.setIsAlive(member.get("isAlive").getAsBoolean());
}
if (member.get("linkedClientId") != null && !member.get("linkedClientId").isJsonNull()) {
familyMember.setLinkedClient(this.clientRepositoryWrapper.findOneWithNotFoundDetection(member.get("linkedClientId").getAsLong()));
}
if (member.get("marriageDate") != null && !member.get("marriageDate").isJsonNull()) {
try {
DateTimeFormatter fmt = new DateTimeFormatterBuilder().appendPattern(member.get("dateFormat").getAsString()).toFormatter();
familyMember.setMarriageDate(LocalDate.parse(member.get("marriageDate").getAsString(), fmt));
} catch (DateTimeParseException e) {
LOG.warn("Could not parse marriageDate", e);
}
}
if (member.get("marriageAuthority") != null && !member.get("marriageAuthority").isJsonNull()) {
familyMember.setMarriageAuthority(member.get("marriageAuthority").getAsString());
}
this.clientFamilyRepository.saveAndFlush(familyMember);
}
@@ -378,10 +419,33 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
clientFamilyMember.setRemarks(command.stringValueOfParameterNamed("remarks"));
}
// ClientFamilyMembers
// clientFamilyMembers=ClientFamilyMembers.fromJson(client, firstName,
// middleName, lastName, qualification, relationship, maritalStatus,
// gender, dateOfBirth, profession);
// CBS extended fields
if (command.parameterExists("aadhaarNumber")) {
clientFamilyMember.setAadhaarNumber(command.stringValueOfParameterNamed("aadhaarNumber"));
}
if (command.parameterExists("panNumber")) {
clientFamilyMember.setPanNumber(command.stringValueOfParameterNamed("panNumber"));
}
if (command.parameterExists("nationality")) {
clientFamilyMember.setNationality(command.stringValueOfParameterNamed("nationality"));
}
if (command.parameterExists("isAlive")) {
clientFamilyMember.setIsAlive(command.booleanObjectValueOfParameterNamed("isAlive"));
}
if (command.parameterExists("marriageDate")) {
clientFamilyMember.setMarriageDate(command.localDateValueOfParameterNamed("marriageDate"));
}
if (command.parameterExists("marriageAuthority")) {
clientFamilyMember.setMarriageAuthority(command.stringValueOfParameterNamed("marriageAuthority"));
}
if (command.parameterExists("linkedClientId")) {
final Long linkedId = command.longValueOfParameterNamed("linkedClientId");
if (linkedId != null) {
clientFamilyMember.setLinkedClient(this.clientRepositoryWrapper.findOneWithNotFoundDetection(linkedId));
} else {
clientFamilyMember.setLinkedClient(null);
}
}
this.clientFamilyRepository.saveAndFlush(clientFamilyMember);
@@ -0,0 +1,29 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.client.service;
import java.util.List;
import org.apache.fineract.portfolio.client.data.ClientForm60Data;
public interface ClientForm60ReadPlatformService {
List<ClientForm60Data> retrieveAllByClientId(Long clientId);
ClientForm60Data retrieveOne(Long clientId, Long form60Id);
}
@@ -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.service;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.client.data.ClientForm60Data;
import org.apache.fineract.portfolio.client.domain.ClientForm60;
import org.apache.fineract.portfolio.client.domain.ClientForm60Repository;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ClientForm60ReadPlatformServiceImpl implements ClientForm60ReadPlatformService {
private final ClientForm60Repository form60Repository;
@Override
public List<ClientForm60Data> retrieveAllByClientId(final Long clientId) {
return form60Repository.findByClientId(clientId).stream()
.map(this::toData)
.collect(Collectors.toList());
}
@Override
public ClientForm60Data retrieveOne(final Long clientId, final Long form60Id) {
final ClientForm60 form60 = form60Repository.findByIdAndClientId(form60Id, clientId)
.orElseThrow(() -> new AbstractPlatformResourceNotFoundException("error.msg.client.form60.not.found",
"Form 60 record with identifier " + form60Id + " does not exist", form60Id) {});
return toData(form60);
}
private ClientForm60Data toData(final ClientForm60 f) {
return ClientForm60Data.builder()
.id(f.getId())
.clientId(f.getClient().getId())
.clientDisplayName(f.getClient().getDisplayName())
.fullName(f.getFullName())
.dateOfBirth(f.getDateOfBirth())
.fatherName(f.getFatherName())
.address(f.getAddress())
.transactionType(f.getTransactionType())
.transactionAmount(f.getTransactionAmount())
.estimatedTotalIncome(f.getEstimatedTotalIncome())
.reasonForNoPan(f.getReasonForNoPan())
.declaration(f.isDeclaration())
.submittedOn(f.getSubmittedOn())
.build();
}
}

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