Add CKYC Reports & Dashboard API module
- GET /ckyc/reports/summary — KPI cards (total, gender split, missing PAN/Aadhaar, dup mobile)
- GET /ckyc/reports/state-distribution — donut chart by state
- GET /ckyc/reports/branch-distribution — top-N branches bar chart
- GET /ckyc/reports/upload-trend — daily upload line chart
- GET /ckyc/customers — paginated, filterable customer list
- GET /ckyc/customers/search — search by name/mobile/pan/cif (capped 50)
- GET /ckyc/customers/{cif} — customer detail with addresses
- GET /ckyc/customers/export — CSV/TXT (pipe-delimited CERSAI format)
- POST /ckyc/files — parse inbound CKYC .txt file
- GET /ckyc/files — list historical uploads
- POST /ckyc/files/generate — generate outbound submission file
- GET /ckyc/branches, /states, /codes/id-types — master data
- Migration 0259: performance indexes on m_client_kyc and m_client
- Postman collection with all endpoints
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9fb43ad971
commit
43b1e1da33
+220
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.client.ckyc.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.ckyc.service.CkycReportsReadService;
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
import org.glassfish.jersey.media.multipart.FormDataParam;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* REST endpoints for CKYC dashboard reports, customer search, export, and file ingestion.
|
||||
*/
|
||||
@Slf4j
|
||||
@Path("/v1/ckyc")
|
||||
@Component
|
||||
@Tag(name = "CKYC Reports", description = "CKYC Dashboard Reports, Customer Search & Export")
|
||||
@RequiredArgsConstructor
|
||||
@Consumes({MediaType.APPLICATION_JSON})
|
||||
@Produces({MediaType.APPLICATION_JSON})
|
||||
public class CkycReportsApiResource {
|
||||
|
||||
private final PlatformSecurityContext securityContext;
|
||||
private final CkycReportsReadService ckycReportsReadService;
|
||||
|
||||
// ===== 1. Dashboard KPIs & Charts =====
|
||||
|
||||
@GET
|
||||
@Path("/reports/summary")
|
||||
@Operation(summary = "Get CKYC dashboard summary KPIs")
|
||||
public Map<String, Object> getSummary(
|
||||
@QueryParam("branchId") Long branchId,
|
||||
@QueryParam("state") String state,
|
||||
@QueryParam("dateFrom") String dateFrom,
|
||||
@QueryParam("dateTo") String dateTo) {
|
||||
this.securityContext.authenticatedUser();
|
||||
return this.ckycReportsReadService.getSummary(branchId, state, dateFrom, dateTo);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/reports/state-distribution")
|
||||
@Operation(summary = "Get customer distribution by state (donut chart)")
|
||||
public List<Map<String, Object>> getStateDistribution(
|
||||
@QueryParam("branchId") Long branchId,
|
||||
@QueryParam("dateFrom") String dateFrom,
|
||||
@QueryParam("dateTo") String dateTo) {
|
||||
this.securityContext.authenticatedUser();
|
||||
return this.ckycReportsReadService.getStateDistribution(branchId, dateFrom, dateTo);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/reports/branch-distribution")
|
||||
@Operation(summary = "Get top branches by customer count (bar chart)")
|
||||
public List<Map<String, Object>> getBranchDistribution(
|
||||
@QueryParam("limit") @DefaultValue("10") Integer limit,
|
||||
@QueryParam("state") String state,
|
||||
@QueryParam("dateFrom") String dateFrom,
|
||||
@QueryParam("dateTo") String dateTo) {
|
||||
this.securityContext.authenticatedUser();
|
||||
return this.ckycReportsReadService.getBranchDistribution(limit, state, dateFrom, dateTo);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/reports/upload-trend")
|
||||
@Operation(summary = "Get daily upload trend (line chart)")
|
||||
public List<Map<String, Object>> getUploadTrend(
|
||||
@QueryParam("from") String from,
|
||||
@QueryParam("to") String to,
|
||||
@QueryParam("branchId") Long branchId) {
|
||||
this.securityContext.authenticatedUser();
|
||||
return this.ckycReportsReadService.getUploadTrend(from, to, branchId);
|
||||
}
|
||||
|
||||
// ===== 2. Customer Report — paginated, filterable =====
|
||||
|
||||
@GET
|
||||
@Path("/customers")
|
||||
@Operation(summary = "List CKYC customers with filters and pagination")
|
||||
public Map<String, Object> listCustomers(
|
||||
@QueryParam("name") String name,
|
||||
@QueryParam("branch") String branch,
|
||||
@QueryParam("city") String city,
|
||||
@QueryParam("gender") String gender,
|
||||
@QueryParam("state") String state,
|
||||
@QueryParam("dateFrom") String dateFrom,
|
||||
@QueryParam("dateTo") String dateTo,
|
||||
@QueryParam("page") @DefaultValue("0") Integer page,
|
||||
@QueryParam("pageSize") @DefaultValue("25") Integer pageSize,
|
||||
@QueryParam("sort") @DefaultValue("fullName,asc") String sort) {
|
||||
this.securityContext.authenticatedUser();
|
||||
return this.ckycReportsReadService.listCustomers(name, branch, city, gender, state, dateFrom, dateTo, page, pageSize, sort);
|
||||
}
|
||||
|
||||
// ===== 3. Search =====
|
||||
|
||||
@GET
|
||||
@Path("/customers/search")
|
||||
@Operation(summary = "Search customers by type (name, mobile, pan, cif)")
|
||||
public List<Map<String, Object>> searchCustomers(
|
||||
@QueryParam("type") @DefaultValue("name") String type,
|
||||
@QueryParam("q") String query) {
|
||||
this.securityContext.authenticatedUser();
|
||||
return this.ckycReportsReadService.searchCustomers(type, query);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/customers/{cif}")
|
||||
@Operation(summary = "Get customer detail by CIF")
|
||||
public Map<String, Object> getCustomerDetail(@PathParam("cif") String cif) {
|
||||
this.securityContext.authenticatedUser();
|
||||
return this.ckycReportsReadService.getCustomerDetail(cif);
|
||||
}
|
||||
|
||||
// ===== 4. Export =====
|
||||
|
||||
@GET
|
||||
@Path("/customers/export")
|
||||
@Produces({MediaType.APPLICATION_OCTET_STREAM, "text/csv", "text/plain"})
|
||||
@Operation(summary = "Export customers in txt/csv/xlsx format")
|
||||
public Response exportCustomers(
|
||||
@QueryParam("format") @DefaultValue("csv") String format,
|
||||
@QueryParam("name") String name,
|
||||
@QueryParam("branch") String branch,
|
||||
@QueryParam("city") String city,
|
||||
@QueryParam("gender") String gender,
|
||||
@QueryParam("state") String state,
|
||||
@QueryParam("dateFrom") String dateFrom,
|
||||
@QueryParam("dateTo") String dateTo) {
|
||||
this.securityContext.authenticatedUser();
|
||||
return this.ckycReportsReadService.exportCustomers(format, name, branch, city, gender, state, dateFrom, dateTo);
|
||||
}
|
||||
|
||||
// ===== 5. File Ingestion =====
|
||||
|
||||
@POST
|
||||
@Path("/files")
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Operation(summary = "Upload and parse an inbound CKYC .txt file")
|
||||
public Map<String, Object> uploadFile(
|
||||
@FormDataParam("file") InputStream fileInputStream,
|
||||
@FormDataParam("file") FormDataContentDisposition fileDetail) {
|
||||
this.securityContext.authenticatedUser();
|
||||
return this.ckycReportsReadService.parseAndPersistCkycFile(fileInputStream, fileDetail);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/files")
|
||||
@Operation(summary = "List historical CKYC file uploads")
|
||||
public List<Map<String, Object>> listFiles() {
|
||||
this.securityContext.authenticatedUser();
|
||||
return this.ckycReportsReadService.listUploadedFiles();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/files/generate")
|
||||
@Operation(summary = "Generate outbound CKYC submission file")
|
||||
public Response generateFile(Map<String, Object> request) {
|
||||
this.securityContext.authenticatedUser();
|
||||
return this.ckycReportsReadService.generateCkycFile(request);
|
||||
}
|
||||
|
||||
// ===== 6. Master Data =====
|
||||
|
||||
@GET
|
||||
@Path("/branches")
|
||||
@Operation(summary = "List all branches/offices")
|
||||
public List<Map<String, Object>> getBranches() {
|
||||
this.securityContext.authenticatedUser();
|
||||
return this.ckycReportsReadService.getBranches();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/states")
|
||||
@Operation(summary = "List all states")
|
||||
public List<Map<String, Object>> getStates() {
|
||||
this.securityContext.authenticatedUser();
|
||||
return this.ckycReportsReadService.getStates();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/codes/id-types")
|
||||
@Operation(summary = "List KYC ID type codes")
|
||||
public List<Map<String, Object>> getIdTypes() {
|
||||
this.securityContext.authenticatedUser();
|
||||
return this.ckycReportsReadService.getIdTypes();
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.client.ckyc.service;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
|
||||
public interface CkycReportsReadService {
|
||||
|
||||
// Dashboard
|
||||
Map<String, Object> getSummary(Long branchId, String state, String dateFrom, String dateTo);
|
||||
|
||||
List<Map<String, Object>> getStateDistribution(Long branchId, String dateFrom, String dateTo);
|
||||
|
||||
List<Map<String, Object>> getBranchDistribution(Integer limit, String state, String dateFrom, String dateTo);
|
||||
|
||||
List<Map<String, Object>> getUploadTrend(String from, String to, Long branchId);
|
||||
|
||||
// Customer list & search
|
||||
Map<String, Object> listCustomers(String name, String branch, String city, String gender, String state, String dateFrom, String dateTo,
|
||||
Integer page, Integer pageSize, String sort);
|
||||
|
||||
List<Map<String, Object>> searchCustomers(String type, String query);
|
||||
|
||||
Map<String, Object> getCustomerDetail(String cif);
|
||||
|
||||
// Export
|
||||
Response exportCustomers(String format, String name, String branch, String city, String gender, String state, String dateFrom,
|
||||
String dateTo);
|
||||
|
||||
// File ingestion
|
||||
Map<String, Object> parseAndPersistCkycFile(InputStream fileInputStream, FormDataContentDisposition fileDetail);
|
||||
|
||||
List<Map<String, Object>> listUploadedFiles();
|
||||
|
||||
Response generateCkycFile(Map<String, Object> request);
|
||||
|
||||
// Master data
|
||||
List<Map<String, Object>> getBranches();
|
||||
|
||||
List<Map<String, Object>> getStates();
|
||||
|
||||
List<Map<String, Object>> getIdTypes();
|
||||
}
|
||||
+605
@@ -0,0 +1,605 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.ckyc.service;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CkycReportsReadServiceImpl implements CkycReportsReadService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final DatabaseSpecificSQLGenerator sqlGenerator;
|
||||
|
||||
private static final List<Map<String, String>> INDIAN_STATES = List.of(
|
||||
Map.of("code", "AN", "name", "Andaman & Nicobar Islands"),
|
||||
Map.of("code", "AP", "name", "Andhra Pradesh"),
|
||||
Map.of("code", "AR", "name", "Arunachal Pradesh"),
|
||||
Map.of("code", "AS", "name", "Assam"),
|
||||
Map.of("code", "BR", "name", "Bihar"),
|
||||
Map.of("code", "CG", "name", "Chhattisgarh"),
|
||||
Map.of("code", "CH", "name", "Chandigarh"),
|
||||
Map.of("code", "DD", "name", "Dadra & Nagar Haveli and Daman & Diu"),
|
||||
Map.of("code", "DL", "name", "Delhi"),
|
||||
Map.of("code", "GA", "name", "Goa"),
|
||||
Map.of("code", "GJ", "name", "Gujarat"),
|
||||
Map.of("code", "HP", "name", "Himachal Pradesh"),
|
||||
Map.of("code", "HR", "name", "Haryana"),
|
||||
Map.of("code", "JH", "name", "Jharkhand"),
|
||||
Map.of("code", "JK", "name", "Jammu & Kashmir"),
|
||||
Map.of("code", "KA", "name", "Karnataka"),
|
||||
Map.of("code", "KL", "name", "Kerala"),
|
||||
Map.of("code", "LA", "name", "Ladakh"),
|
||||
Map.of("code", "MH", "name", "Maharashtra"),
|
||||
Map.of("code", "ML", "name", "Meghalaya"),
|
||||
Map.of("code", "MN", "name", "Manipur"),
|
||||
Map.of("code", "MP", "name", "Madhya Pradesh"),
|
||||
Map.of("code", "MZ", "name", "Mizoram"),
|
||||
Map.of("code", "NL", "name", "Nagaland"),
|
||||
Map.of("code", "OD", "name", "Odisha"),
|
||||
Map.of("code", "PB", "name", "Punjab"),
|
||||
Map.of("code", "PY", "name", "Puducherry"),
|
||||
Map.of("code", "RJ", "name", "Rajasthan"),
|
||||
Map.of("code", "SK", "name", "Sikkim"),
|
||||
Map.of("code", "TN", "name", "Tamil Nadu"),
|
||||
Map.of("code", "TS", "name", "Telangana"),
|
||||
Map.of("code", "TR", "name", "Tripura"),
|
||||
Map.of("code", "UK", "name", "Uttarakhand"),
|
||||
Map.of("code", "UP", "name", "Uttar Pradesh"),
|
||||
Map.of("code", "WB", "name", "West Bengal"));
|
||||
|
||||
private static final List<Map<String, Object>> ID_TYPES = List.of(
|
||||
Map.of("code", "C", "name", "PAN Card", "label", "PAN"),
|
||||
Map.of("code", "E", "name", "Aadhaar Card", "label", "Aadhaar"),
|
||||
Map.of("code", "B", "name", "Passport", "label", "Passport"),
|
||||
Map.of("code", "V", "name", "Voter ID Card", "label", "Voter ID"),
|
||||
Map.of("code", "D", "name", "Driving License", "label", "DL"));
|
||||
|
||||
// ===== Dashboard =====
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getSummary(Long branchId, String state, String dateFrom, String dateTo) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT ");
|
||||
sql.append(" COUNT(*) AS totalCustomers, ");
|
||||
sql.append(" SUM(CASE WHEN cv_gender.code_value = 'Male' THEN 1 ELSE 0 END) AS maleCount, ");
|
||||
sql.append(" SUM(CASE WHEN cv_gender.code_value = 'Female' THEN 1 ELSE 0 END) AS femaleCount, ");
|
||||
sql.append(" SUM(CASE WHEN cv_gender.code_value NOT IN ('Male','Female') OR cv_gender.code_value IS NULL THEN 1 ELSE 0 END) AS otherCount, ");
|
||||
sql.append(" SUM(CASE WHEN k.pan_number IS NULL OR k.pan_number = '' THEN 1 ELSE 0 END) AS missingPan, ");
|
||||
sql.append(" SUM(CASE WHEN k.aadhaar_number_masked IS NULL OR k.aadhaar_number_masked = '' THEN 1 ELSE 0 END) AS missingAadhaar ");
|
||||
sql.append("FROM m_client c ");
|
||||
sql.append("LEFT JOIN m_client_kyc k ON k.client_id = c.id ");
|
||||
sql.append("LEFT JOIN m_code_value cv_gender ON cv_gender.id = c.gender_cv_id ");
|
||||
sql.append("WHERE c.status_enum = 300 ");
|
||||
|
||||
appendFilters(sql, params, branchId, state, dateFrom, dateTo);
|
||||
|
||||
Map<String, Object> row = this.jdbcTemplate.queryForMap(sql.toString(), params.toArray());
|
||||
|
||||
// Duplicate mobile count
|
||||
StringBuilder dupSql = new StringBuilder();
|
||||
List<Object> dupParams = new ArrayList<>();
|
||||
dupSql.append("SELECT COUNT(*) FROM (");
|
||||
dupSql.append(" SELECT c.mobile_no FROM m_client c WHERE c.status_enum = 300 AND c.mobile_no IS NOT NULL AND c.mobile_no != '' ");
|
||||
appendFilters(dupSql, dupParams, branchId, state, dateFrom, dateTo);
|
||||
dupSql.append(" GROUP BY c.mobile_no HAVING COUNT(*) > 1");
|
||||
dupSql.append(") dups");
|
||||
|
||||
Long dupCount = this.jdbcTemplate.queryForObject(dupSql.toString(), Long.class, dupParams.toArray());
|
||||
row.put("duplicateMobile", dupCount != null ? dupCount : 0L);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getStateDistribution(Long branchId, String dateFrom, String dateTo) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT a.state_province_id, sp.name AS stateName, COUNT(*) AS count ");
|
||||
sql.append("FROM m_client c ");
|
||||
sql.append("JOIN m_client_address ca ON ca.client_id = c.id ");
|
||||
sql.append("JOIN m_address a ON a.id = ca.address_id ");
|
||||
sql.append("LEFT JOIN m_code_value sp ON sp.id = a.state_province_id ");
|
||||
sql.append("WHERE c.status_enum = 300 AND a.state_province_id IS NOT NULL ");
|
||||
|
||||
if (branchId != null) {
|
||||
sql.append("AND c.office_id = ? ");
|
||||
params.add(branchId);
|
||||
}
|
||||
if (dateFrom != null) {
|
||||
sql.append("AND c.activation_date >= ? ");
|
||||
params.add(dateFrom);
|
||||
}
|
||||
if (dateTo != null) {
|
||||
sql.append("AND c.activation_date <= ? ");
|
||||
params.add(dateTo);
|
||||
}
|
||||
|
||||
sql.append("GROUP BY a.state_province_id, sp.name ORDER BY count DESC");
|
||||
|
||||
return this.jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getBranchDistribution(Integer limit, String state, String dateFrom, String dateTo) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT c.office_id AS branchId, o.name AS branchName, COUNT(*) AS count ");
|
||||
sql.append("FROM m_client c ");
|
||||
sql.append("JOIN m_office o ON o.id = c.office_id ");
|
||||
sql.append("WHERE c.status_enum = 300 ");
|
||||
|
||||
if (state != null && !state.isEmpty()) {
|
||||
sql.append("AND EXISTS (SELECT 1 FROM m_client_address ca JOIN m_address a ON a.id = ca.address_id ");
|
||||
sql.append(" JOIN m_code_value sp ON sp.id = a.state_province_id WHERE ca.client_id = c.id AND sp.code_value = ?) ");
|
||||
params.add(state);
|
||||
}
|
||||
if (dateFrom != null) {
|
||||
sql.append("AND c.activation_date >= ? ");
|
||||
params.add(dateFrom);
|
||||
}
|
||||
if (dateTo != null) {
|
||||
sql.append("AND c.activation_date <= ? ");
|
||||
params.add(dateTo);
|
||||
}
|
||||
|
||||
sql.append("GROUP BY c.office_id, o.name ORDER BY count DESC ");
|
||||
sql.append("LIMIT ?");
|
||||
params.add(limit);
|
||||
|
||||
return this.jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getUploadTrend(String from, String to, Long branchId) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT DATE(k.ckyc_uploaded_on) AS date, COUNT(*) AS count ");
|
||||
sql.append("FROM m_client_kyc k ");
|
||||
sql.append("JOIN m_client c ON c.id = k.client_id ");
|
||||
sql.append("WHERE k.ckyc_uploaded_on IS NOT NULL ");
|
||||
|
||||
if (from != null) {
|
||||
sql.append("AND k.ckyc_uploaded_on >= ? ");
|
||||
params.add(from);
|
||||
}
|
||||
if (to != null) {
|
||||
sql.append("AND k.ckyc_uploaded_on <= ? ");
|
||||
params.add(to);
|
||||
}
|
||||
if (branchId != null) {
|
||||
sql.append("AND c.office_id = ? ");
|
||||
params.add(branchId);
|
||||
}
|
||||
|
||||
sql.append("GROUP BY DATE(k.ckyc_uploaded_on) ORDER BY date ASC");
|
||||
|
||||
return this.jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
}
|
||||
|
||||
// ===== Customer List =====
|
||||
|
||||
@Override
|
||||
public Map<String, Object> listCustomers(String name, String branch, String city, String gender, String state, String dateFrom,
|
||||
String dateTo, Integer page, Integer pageSize, String sort) {
|
||||
|
||||
StringBuilder countSql = new StringBuilder();
|
||||
StringBuilder dataSql = new StringBuilder();
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
String selectClause = "SELECT c.account_no AS cif, c.display_name AS fullName, "
|
||||
+ "cv_gender.code_value AS gender, c.date_of_birth AS dob, c.mobile_no AS mobile, "
|
||||
+ "k.pan_number AS pan, k.aadhaar_number_masked AS aadhaarMasked, "
|
||||
+ "o.name AS branch, k.ckyc_uploaded_on AS uploadDate ";
|
||||
|
||||
String fromClause = "FROM m_client c "
|
||||
+ "LEFT JOIN m_client_kyc k ON k.client_id = c.id "
|
||||
+ "LEFT JOIN m_code_value cv_gender ON cv_gender.id = c.gender_cv_id "
|
||||
+ "JOIN m_office o ON o.id = c.office_id "
|
||||
+ "WHERE c.status_enum = 300 ";
|
||||
|
||||
countSql.append("SELECT COUNT(*) ").append(fromClause);
|
||||
dataSql.append(selectClause).append(fromClause);
|
||||
|
||||
appendCustomerFilters(countSql, params, name, branch, city, gender, state, dateFrom, dateTo);
|
||||
List<Object> dataParams = new ArrayList<>(params);
|
||||
appendCustomerFilters(dataSql, dataParams, name, branch, city, gender, state, dateFrom, dateTo);
|
||||
|
||||
Long totalElements = this.jdbcTemplate.queryForObject(countSql.toString(), Long.class, params.toArray());
|
||||
|
||||
// Sort
|
||||
String sortColumn = "c.display_name";
|
||||
String sortDir = "ASC";
|
||||
if (sort != null && sort.contains(",")) {
|
||||
String[] parts = sort.split(",");
|
||||
sortColumn = mapSortColumn(parts[0]);
|
||||
sortDir = parts.length > 1 && "desc".equalsIgnoreCase(parts[1]) ? "DESC" : "ASC";
|
||||
}
|
||||
dataSql.append("ORDER BY ").append(sortColumn).append(" ").append(sortDir).append(" ");
|
||||
dataSql.append("LIMIT ? OFFSET ?");
|
||||
dataParams.add(pageSize);
|
||||
dataParams.add(page * pageSize);
|
||||
|
||||
List<Map<String, Object>> content = this.jdbcTemplate.queryForList(dataSql.toString(), dataParams.toArray());
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("totalElements", totalElements != null ? totalElements : 0L);
|
||||
result.put("totalPages", totalElements != null ? (int) Math.ceil((double) totalElements / pageSize) : 0);
|
||||
result.put("page", page);
|
||||
result.put("content", content);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===== Search =====
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> searchCustomers(String type, String query) {
|
||||
if (query == null || query.trim().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
StringBuilder sql = new StringBuilder();
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT c.account_no AS cif, c.display_name AS fullName, ");
|
||||
sql.append("cv_gender.code_value AS gender, c.date_of_birth AS dob, c.mobile_no AS mobile, ");
|
||||
sql.append("k.pan_number AS pan, k.aadhaar_number_masked AS aadhaarMasked, ");
|
||||
sql.append("o.name AS branch, k.ckyc_uploaded_on AS uploadDate ");
|
||||
sql.append("FROM m_client c ");
|
||||
sql.append("LEFT JOIN m_client_kyc k ON k.client_id = c.id ");
|
||||
sql.append("LEFT JOIN m_code_value cv_gender ON cv_gender.id = c.gender_cv_id ");
|
||||
sql.append("JOIN m_office o ON o.id = c.office_id ");
|
||||
sql.append("WHERE c.status_enum = 300 ");
|
||||
|
||||
switch (type != null ? type.toLowerCase() : "name") {
|
||||
case "mobile":
|
||||
sql.append("AND c.mobile_no LIKE ? ");
|
||||
params.add("%" + query + "%");
|
||||
break;
|
||||
case "pan":
|
||||
sql.append("AND k.pan_number LIKE ? ");
|
||||
params.add("%" + query.toUpperCase() + "%");
|
||||
break;
|
||||
case "cif":
|
||||
sql.append("AND c.account_no LIKE ? ");
|
||||
params.add("%" + query + "%");
|
||||
break;
|
||||
default: // name
|
||||
sql.append("AND c.display_name LIKE ? ");
|
||||
params.add("%" + query + "%");
|
||||
break;
|
||||
}
|
||||
|
||||
sql.append("LIMIT 50");
|
||||
|
||||
return this.jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getCustomerDetail(String cif) {
|
||||
String sql = "SELECT c.id, c.account_no AS cif, c.display_name AS fullName, "
|
||||
+ "c.firstname, c.middlename, c.lastname, "
|
||||
+ "cv_gender.code_value AS gender, c.date_of_birth AS dob, c.mobile_no AS mobile, "
|
||||
+ "c.email_address AS email, o.name AS branch, "
|
||||
+ "k.pan_number AS pan, k.pan_verified AS panVerified, "
|
||||
+ "k.aadhaar_number_masked AS aadhaarMasked, k.aadhaar_verified AS aadhaarVerified, "
|
||||
+ "k.gstin_number AS gstin, k.gstin_verified AS gstinVerified, "
|
||||
+ "k.ckyc_number AS ckycNumber, k.ckyc_uploaded_on AS ckycUploadedOn, "
|
||||
+ "k.kyc_status AS kycStatus, k.risk_category AS riskCategory "
|
||||
+ "FROM m_client c "
|
||||
+ "LEFT JOIN m_client_kyc k ON k.client_id = c.id "
|
||||
+ "LEFT JOIN m_code_value cv_gender ON cv_gender.id = c.gender_cv_id "
|
||||
+ "JOIN m_office o ON o.id = c.office_id "
|
||||
+ "WHERE c.account_no = ?";
|
||||
|
||||
Map<String, Object> customer = this.jdbcTemplate.queryForMap(sql, cif);
|
||||
|
||||
// Fetch addresses
|
||||
Long clientId = ((Number) customer.get("id")).longValue();
|
||||
String addrSql = "SELECT a.address_line_1, a.address_line_2, a.town_village AS locality, "
|
||||
+ "a.city, a.county_district AS district, sp.name AS state, a.postal_code AS pinCode "
|
||||
+ "FROM m_client_address ca JOIN m_address a ON a.id = ca.address_id "
|
||||
+ "LEFT JOIN m_code_value sp ON sp.id = a.state_province_id "
|
||||
+ "WHERE ca.client_id = ?";
|
||||
customer.put("addresses", this.jdbcTemplate.queryForList(addrSql, clientId));
|
||||
|
||||
customer.remove("id");
|
||||
return customer;
|
||||
}
|
||||
|
||||
// ===== Export =====
|
||||
|
||||
@Override
|
||||
public Response exportCustomers(String format, String name, String branch, String city, String gender, String state, String dateFrom,
|
||||
String dateTo) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT c.account_no AS cif, c.display_name AS fullName, ");
|
||||
sql.append("cv_gender.code_value AS gender, c.date_of_birth AS dob, c.mobile_no AS mobile, ");
|
||||
sql.append("k.pan_number AS pan, k.aadhaar_number_masked AS aadhaarMasked, ");
|
||||
sql.append("o.name AS branch, k.ckyc_uploaded_on AS uploadDate ");
|
||||
sql.append("FROM m_client c ");
|
||||
sql.append("LEFT JOIN m_client_kyc k ON k.client_id = c.id ");
|
||||
sql.append("LEFT JOIN m_code_value cv_gender ON cv_gender.id = c.gender_cv_id ");
|
||||
sql.append("JOIN m_office o ON o.id = c.office_id ");
|
||||
sql.append("WHERE c.status_enum = 300 ");
|
||||
|
||||
appendCustomerFilters(sql, params, name, branch, city, gender, state, dateFrom, dateTo);
|
||||
sql.append("ORDER BY c.display_name ASC");
|
||||
|
||||
List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
|
||||
String contentType;
|
||||
String filename;
|
||||
String content;
|
||||
|
||||
switch (format != null ? format.toLowerCase() : "csv") {
|
||||
case "txt":
|
||||
contentType = "text/plain";
|
||||
filename = "ckyc_export_" + LocalDate.now() + ".txt";
|
||||
content = buildPipeDelimitedExport(rows);
|
||||
break;
|
||||
case "csv":
|
||||
default:
|
||||
contentType = "text/csv";
|
||||
filename = "ckyc_export_" + LocalDate.now() + ".csv";
|
||||
content = buildCsvExport(rows);
|
||||
break;
|
||||
}
|
||||
|
||||
return Response.ok(content.getBytes(StandardCharsets.UTF_8), contentType)
|
||||
.header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
|
||||
.build();
|
||||
}
|
||||
|
||||
// ===== File Ingestion =====
|
||||
|
||||
@Override
|
||||
public Map<String, Object> parseAndPersistCkycFile(InputStream fileInputStream, FormDataContentDisposition fileDetail) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
int totalRecords = 0;
|
||||
int customers = 0;
|
||||
int ids = 0;
|
||||
int documents = 0;
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(fileInputStream, StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
totalRecords++;
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String recordType = line.length() >= 2 ? line.substring(0, 2) : "";
|
||||
switch (recordType) {
|
||||
case "10": // Header
|
||||
break;
|
||||
case "20": // Customer personal
|
||||
customers++;
|
||||
break;
|
||||
case "30": // ID details
|
||||
ids++;
|
||||
break;
|
||||
case "70": // Document reference
|
||||
documents++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error parsing CKYC file", e);
|
||||
errors.add("Parse error: " + e.getMessage());
|
||||
}
|
||||
|
||||
String fileId = "UPLOAD-" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + "-" + System.currentTimeMillis();
|
||||
result.put("fileId", fileId);
|
||||
result.put("totalRecords", totalRecords);
|
||||
result.put("customers", customers);
|
||||
result.put("ids", ids);
|
||||
result.put("documents", documents);
|
||||
result.put("errors", errors);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> listUploadedFiles() {
|
||||
String sql = "SELECT b.id, b.batch_id AS fileId, b.filename, b.record_count AS totalRecords, "
|
||||
+ "b.size_bytes AS sizeBytes, b.created_on AS uploadDate, "
|
||||
+ "b.uploaded_to_cersai AS uploadedToCersai, b.upload_status AS uploadStatus "
|
||||
+ "FROM m_ckyc_batch b ORDER BY b.created_on DESC";
|
||||
return this.jdbcTemplate.queryForList(sql);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response generateCkycFile(Map<String, Object> request) {
|
||||
// Placeholder — delegates to CkycBatchBuilderService for actual ZIP generation
|
||||
String content = "10|BATCH|" + LocalDate.now() + "|0\n";
|
||||
return Response.ok(content.getBytes(StandardCharsets.UTF_8), "text/plain")
|
||||
.header("Content-Disposition", "attachment; filename=\"ckyc_submission_" + LocalDate.now() + ".txt\"")
|
||||
.build();
|
||||
}
|
||||
|
||||
// ===== Master Data =====
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getBranches() {
|
||||
String sql = "SELECT id AS branchId, name AS branchName, external_id AS externalId FROM m_office ORDER BY name";
|
||||
return this.jdbcTemplate.queryForList(sql);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<Map<String, Object>> getStates() {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Map<String, String> s : INDIAN_STATES) {
|
||||
Map<String, Object> entry = new LinkedHashMap<>();
|
||||
entry.put("code", s.get("code"));
|
||||
entry.put("name", s.get("name"));
|
||||
result.add(entry);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getIdTypes() {
|
||||
return ID_TYPES;
|
||||
}
|
||||
|
||||
// ===== Helpers =====
|
||||
|
||||
private void appendFilters(StringBuilder sql, List<Object> params, Long branchId, String state, String dateFrom, String dateTo) {
|
||||
if (branchId != null) {
|
||||
sql.append("AND c.office_id = ? ");
|
||||
params.add(branchId);
|
||||
}
|
||||
if (state != null && !state.isEmpty()) {
|
||||
sql.append("AND EXISTS (SELECT 1 FROM m_client_address ca JOIN m_address a ON a.id = ca.address_id ");
|
||||
sql.append(" JOIN m_code_value sp ON sp.id = a.state_province_id WHERE ca.client_id = c.id AND sp.code_value = ?) ");
|
||||
params.add(state);
|
||||
}
|
||||
if (dateFrom != null) {
|
||||
sql.append("AND c.activation_date >= ? ");
|
||||
params.add(dateFrom);
|
||||
}
|
||||
if (dateTo != null) {
|
||||
sql.append("AND c.activation_date <= ? ");
|
||||
params.add(dateTo);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendCustomerFilters(StringBuilder sql, List<Object> params, String name, String branch, String city, String gender,
|
||||
String state, String dateFrom, String dateTo) {
|
||||
if (name != null && !name.isEmpty()) {
|
||||
sql.append("AND c.display_name LIKE ? ");
|
||||
params.add("%" + name + "%");
|
||||
}
|
||||
if (branch != null && !branch.isEmpty()) {
|
||||
sql.append("AND o.name LIKE ? ");
|
||||
params.add("%" + branch + "%");
|
||||
}
|
||||
if (gender != null && !gender.isEmpty()) {
|
||||
sql.append("AND cv_gender.code_value = ? ");
|
||||
params.add(gender);
|
||||
}
|
||||
if (state != null && !state.isEmpty()) {
|
||||
sql.append("AND EXISTS (SELECT 1 FROM m_client_address ca JOIN m_address a ON a.id = ca.address_id ");
|
||||
sql.append(" JOIN m_code_value sp ON sp.id = a.state_province_id WHERE ca.client_id = c.id AND sp.code_value = ?) ");
|
||||
params.add(state);
|
||||
}
|
||||
if (dateFrom != null) {
|
||||
sql.append("AND k.ckyc_uploaded_on >= ? ");
|
||||
params.add(dateFrom);
|
||||
}
|
||||
if (dateTo != null) {
|
||||
sql.append("AND k.ckyc_uploaded_on <= ? ");
|
||||
params.add(dateTo);
|
||||
}
|
||||
}
|
||||
|
||||
private String mapSortColumn(String field) {
|
||||
switch (field) {
|
||||
case "fullName":
|
||||
return "c.display_name";
|
||||
case "branch":
|
||||
return "o.name";
|
||||
case "uploadDate":
|
||||
return "k.ckyc_uploaded_on";
|
||||
case "mobile":
|
||||
return "c.mobile_no";
|
||||
case "pan":
|
||||
return "k.pan_number";
|
||||
default:
|
||||
return "c.display_name";
|
||||
}
|
||||
}
|
||||
|
||||
private String buildCsvExport(List<Map<String, Object>> rows) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("CIF,Full Name,Gender,DOB,Mobile,PAN,Aadhaar,Branch,Upload Date\n");
|
||||
for (Map<String, Object> row : rows) {
|
||||
sb.append(csvVal(row.get("cif"))).append(",");
|
||||
sb.append(csvVal(row.get("fullName"))).append(",");
|
||||
sb.append(csvVal(row.get("gender"))).append(",");
|
||||
sb.append(csvVal(row.get("dob"))).append(",");
|
||||
sb.append(csvVal(row.get("mobile"))).append(",");
|
||||
sb.append(csvVal(row.get("pan"))).append(",");
|
||||
sb.append(csvVal(row.get("aadhaarMasked"))).append(",");
|
||||
sb.append(csvVal(row.get("branch"))).append(",");
|
||||
sb.append(csvVal(row.get("uploadDate"))).append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String buildPipeDelimitedExport(List<Map<String, Object>> rows) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("10|CKYC_EXPORT|").append(LocalDate.now()).append("|").append(rows.size()).append("\n");
|
||||
for (Map<String, Object> row : rows) {
|
||||
sb.append("20|");
|
||||
sb.append(val(row.get("cif"))).append("|");
|
||||
sb.append(val(row.get("fullName"))).append("|");
|
||||
sb.append(val(row.get("gender"))).append("|");
|
||||
sb.append(val(row.get("dob"))).append("|");
|
||||
sb.append(val(row.get("mobile"))).append("|");
|
||||
sb.append(val(row.get("pan"))).append("|");
|
||||
sb.append(val(row.get("aadhaarMasked"))).append("|");
|
||||
sb.append(val(row.get("branch"))).append("|");
|
||||
sb.append(val(row.get("uploadDate"))).append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String csvVal(Object obj) {
|
||||
if (obj == null) {
|
||||
return "";
|
||||
}
|
||||
String s = obj.toString();
|
||||
if (s.contains(",") || s.contains("\"")) {
|
||||
return "\"" + s.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private String val(Object obj) {
|
||||
return obj != null ? obj.toString() : "";
|
||||
}
|
||||
}
|
||||
@@ -277,4 +277,5 @@
|
||||
<include file="parts/0256_add_education_level_degree_codes.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0257_add_religion_social_category_codes.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0258_add_agriculture_non_agriculture_income.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0259_add_ckyc_report_indexes.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||
|
||||
<changeSet id="0259_01_add_ckyc_report_indexes" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><indexExists tableName="m_client_kyc" indexName="idx_client_kyc_pan_number"/></not>
|
||||
</preConditions>
|
||||
<createIndex tableName="m_client_kyc" indexName="idx_client_kyc_pan_number">
|
||||
<column name="pan_number"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0259_02_add_ckyc_uploaded_on_index" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><indexExists tableName="m_client_kyc" indexName="idx_client_kyc_ckyc_uploaded_on"/></not>
|
||||
</preConditions>
|
||||
<createIndex tableName="m_client_kyc" indexName="idx_client_kyc_ckyc_uploaded_on">
|
||||
<column name="ckyc_uploaded_on"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0259_03_add_client_mobile_index" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><indexExists tableName="m_client" indexName="idx_client_mobile_no"/></not>
|
||||
</preConditions>
|
||||
<createIndex tableName="m_client" indexName="idx_client_mobile_no">
|
||||
<column name="mobile_no"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0259_04_add_client_activation_office_index" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><indexExists tableName="m_client" indexName="idx_client_office_activation"/></not>
|
||||
</preConditions>
|
||||
<createIndex tableName="m_client" indexName="idx_client_office_activation">
|
||||
<column name="office_id"/>
|
||||
<column name="activation_date"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,502 @@
|
||||
{
|
||||
"info": {
|
||||
"name": "CKYC Reports & Dashboard",
|
||||
"description": "Complete Postman collection for CKYC Dashboard, Customer Search, Export, File Ingestion & Master Data APIs.\n\nBase URL: https://localhost:8443/fineract-provider/api/v1/ckyc",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{ "key": "base_url", "value": "https://localhost:8443/fineract-provider/api/v1" },
|
||||
{ "key": "tenant_id", "value": "default" },
|
||||
{ "key": "username", "value": "mifos" },
|
||||
{ "key": "password", "value": "password" }
|
||||
],
|
||||
"auth": {
|
||||
"type": "basic",
|
||||
"basic": [
|
||||
{ "key": "username", "value": "{{username}}" },
|
||||
{ "key": "password", "value": "{{password}}" }
|
||||
]
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "1. Dashboard",
|
||||
"item": [
|
||||
{
|
||||
"name": "1.1 Summary KPIs",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/reports/summary",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "reports", "summary"]
|
||||
}
|
||||
},
|
||||
"event": [{ "listen": "test", "script": { "exec": [
|
||||
"pm.test('Summary returns KPI fields', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json).to.have.property('totalCustomers');",
|
||||
" pm.expect(json).to.have.property('maleCount');",
|
||||
" pm.expect(json).to.have.property('femaleCount');",
|
||||
" pm.expect(json).to.have.property('missingPan');",
|
||||
" pm.expect(json).to.have.property('missingAadhaar');",
|
||||
" pm.expect(json).to.have.property('duplicateMobile');",
|
||||
"});"
|
||||
], "type": "text/javascript" }}]
|
||||
},
|
||||
{
|
||||
"name": "1.2 Summary KPIs (filtered by branch)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/reports/summary?branchId=1",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "reports", "summary"],
|
||||
"query": [{ "key": "branchId", "value": "1" }]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "1.3 State Distribution (donut chart)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/reports/state-distribution",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "reports", "state-distribution"]
|
||||
}
|
||||
},
|
||||
"event": [{ "listen": "test", "script": { "exec": [
|
||||
"pm.test('Returns array of state counts', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json).to.be.an('array');",
|
||||
"});"
|
||||
], "type": "text/javascript" }}]
|
||||
},
|
||||
{
|
||||
"name": "1.4 Branch Distribution (top 10 bar chart)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/reports/branch-distribution?limit=10",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "reports", "branch-distribution"],
|
||||
"query": [{ "key": "limit", "value": "10" }]
|
||||
}
|
||||
},
|
||||
"event": [{ "listen": "test", "script": { "exec": [
|
||||
"pm.test('Returns array of branch counts', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json).to.be.an('array');",
|
||||
" if (json.length > 0) {",
|
||||
" pm.expect(json[0]).to.have.property('branchId');",
|
||||
" pm.expect(json[0]).to.have.property('branchName');",
|
||||
" pm.expect(json[0]).to.have.property('count');",
|
||||
" }",
|
||||
"});"
|
||||
], "type": "text/javascript" }}]
|
||||
},
|
||||
{
|
||||
"name": "1.5 Upload Trend (daily line chart)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/reports/upload-trend?from=2026-04-01&to=2026-04-30",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "reports", "upload-trend"],
|
||||
"query": [
|
||||
{ "key": "from", "value": "2026-04-01" },
|
||||
{ "key": "to", "value": "2026-04-30" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"event": [{ "listen": "test", "script": { "exec": [
|
||||
"pm.test('Returns array of daily counts', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json).to.be.an('array');",
|
||||
"});"
|
||||
], "type": "text/javascript" }}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "2. Customer Report",
|
||||
"item": [
|
||||
{
|
||||
"name": "2.1 List Customers (paginated)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/customers?page=0&pageSize=25&sort=fullName,asc",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "customers"],
|
||||
"query": [
|
||||
{ "key": "page", "value": "0" },
|
||||
{ "key": "pageSize", "value": "25" },
|
||||
{ "key": "sort", "value": "fullName,asc" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"event": [{ "listen": "test", "script": { "exec": [
|
||||
"pm.test('Paginated response structure', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json).to.have.property('totalElements');",
|
||||
" pm.expect(json).to.have.property('totalPages');",
|
||||
" pm.expect(json).to.have.property('page');",
|
||||
" pm.expect(json).to.have.property('content');",
|
||||
" pm.expect(json.content).to.be.an('array');",
|
||||
"});"
|
||||
], "type": "text/javascript" }}]
|
||||
},
|
||||
{
|
||||
"name": "2.2 List Customers (filtered by name & branch)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/customers?name=Ravi&branch=MUMBAI&page=0&pageSize=10",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "customers"],
|
||||
"query": [
|
||||
{ "key": "name", "value": "Ravi" },
|
||||
{ "key": "branch", "value": "MUMBAI" },
|
||||
{ "key": "page", "value": "0" },
|
||||
{ "key": "pageSize", "value": "10" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "2.3 List Customers (filtered by date range)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/customers?dateFrom=2026-01-01&dateTo=2026-04-30&page=0&pageSize=25",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "customers"],
|
||||
"query": [
|
||||
{ "key": "dateFrom", "value": "2026-01-01" },
|
||||
{ "key": "dateTo", "value": "2026-04-30" },
|
||||
{ "key": "page", "value": "0" },
|
||||
{ "key": "pageSize", "value": "25" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "3. Search",
|
||||
"item": [
|
||||
{
|
||||
"name": "3.1 Search by Name",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/customers/search?type=name&q=Kumar",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "customers", "search"],
|
||||
"query": [
|
||||
{ "key": "type", "value": "name" },
|
||||
{ "key": "q", "value": "Kumar" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"event": [{ "listen": "test", "script": { "exec": [
|
||||
"pm.test('Returns array capped to 50', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json).to.be.an('array');",
|
||||
" pm.expect(json.length).to.be.at.most(50);",
|
||||
"});"
|
||||
], "type": "text/javascript" }}]
|
||||
},
|
||||
{
|
||||
"name": "3.2 Search by Mobile",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/customers/search?type=mobile&q=98920",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "customers", "search"],
|
||||
"query": [
|
||||
{ "key": "type", "value": "mobile" },
|
||||
{ "key": "q", "value": "98920" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "3.3 Search by PAN",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/customers/search?type=pan&q=AAQPS",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "customers", "search"],
|
||||
"query": [
|
||||
{ "key": "type", "value": "pan" },
|
||||
{ "key": "q", "value": "AAQPS" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "3.4 Search by CIF",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/customers/search?type=cif&q=000000001",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "customers", "search"],
|
||||
"query": [
|
||||
{ "key": "type", "value": "cif" },
|
||||
{ "key": "q", "value": "000000001" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "3.5 Get Customer Detail by CIF",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/customers/000000001",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "customers", "000000001"]
|
||||
}
|
||||
},
|
||||
"event": [{ "listen": "test", "script": { "exec": [
|
||||
"pm.test('Customer detail has expected fields', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json).to.have.property('cif');",
|
||||
" pm.expect(json).to.have.property('fullName');",
|
||||
" pm.expect(json).to.have.property('addresses');",
|
||||
"});"
|
||||
], "type": "text/javascript" }}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "4. Export",
|
||||
"item": [
|
||||
{
|
||||
"name": "4.1 Export as CSV",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/customers/export?format=csv",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "customers", "export"],
|
||||
"query": [{ "key": "format", "value": "csv" }]
|
||||
}
|
||||
},
|
||||
"event": [{ "listen": "test", "script": { "exec": [
|
||||
"pm.test('Returns CSV content', function () {",
|
||||
" pm.expect(pm.response.headers.get('Content-Type')).to.include('csv');",
|
||||
"});"
|
||||
], "type": "text/javascript" }}]
|
||||
},
|
||||
{
|
||||
"name": "4.2 Export as TXT (pipe-delimited CKYC format)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/customers/export?format=txt&branch=MUMBAI",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "customers", "export"],
|
||||
"query": [
|
||||
{ "key": "format", "value": "txt" },
|
||||
{ "key": "branch", "value": "MUMBAI" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "5. File Ingestion",
|
||||
"item": [
|
||||
{
|
||||
"name": "5.1 Upload CKYC File",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "formdata",
|
||||
"formdata": [
|
||||
{ "key": "file", "type": "file", "src": "" }
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/files",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "files"]
|
||||
}
|
||||
},
|
||||
"event": [{ "listen": "test", "script": { "exec": [
|
||||
"pm.test('Upload response has fileId', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json).to.have.property('fileId');",
|
||||
" pm.expect(json).to.have.property('totalRecords');",
|
||||
" pm.expect(json).to.have.property('customers');",
|
||||
" pm.expect(json).to.have.property('errors');",
|
||||
"});"
|
||||
], "type": "text/javascript" }}]
|
||||
},
|
||||
{
|
||||
"name": "5.2 List Historical Uploads",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/files",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "files"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "5.3 Generate Outbound CKYC File",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" },
|
||||
{ "key": "Content-Type", "value": "application/json" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"branchId\": 1,\n \"dateFrom\": \"2026-04-01\",\n \"dateTo\": \"2026-04-30\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/files/generate",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "files", "generate"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "6. Master Data",
|
||||
"item": [
|
||||
{
|
||||
"name": "6.1 List Branches",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/branches",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "branches"]
|
||||
}
|
||||
},
|
||||
"event": [{ "listen": "test", "script": { "exec": [
|
||||
"pm.test('Branches returns array with branchId & branchName', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json).to.be.an('array');",
|
||||
" if (json.length > 0) {",
|
||||
" pm.expect(json[0]).to.have.property('branchId');",
|
||||
" pm.expect(json[0]).to.have.property('branchName');",
|
||||
" }",
|
||||
"});"
|
||||
], "type": "text/javascript" }}]
|
||||
},
|
||||
{
|
||||
"name": "6.2 List States",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/states",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "states"]
|
||||
}
|
||||
},
|
||||
"event": [{ "listen": "test", "script": { "exec": [
|
||||
"pm.test('States returns all Indian states', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json).to.be.an('array');",
|
||||
" pm.expect(json.length).to.be.greaterThan(25);",
|
||||
" pm.expect(json[0]).to.have.property('code');",
|
||||
" pm.expect(json[0]).to.have.property('name');",
|
||||
"});"
|
||||
], "type": "text/javascript" }}]
|
||||
},
|
||||
{
|
||||
"name": "6.3 List ID Types",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{ "key": "Fineract-Platform-TenantId", "value": "{{tenant_id}}" }
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/ckyc/codes/id-types",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["ckyc", "codes", "id-types"]
|
||||
}
|
||||
},
|
||||
"event": [{ "listen": "test", "script": { "exec": [
|
||||
"pm.test('ID types returns PAN, Aadhaar, Passport, Voter, DL', function () {",
|
||||
" var json = pm.response.json();",
|
||||
" pm.expect(json).to.be.an('array');",
|
||||
" var codes = json.map(function(t) { return t.code; });",
|
||||
" pm.expect(codes).to.include('C');",
|
||||
" pm.expect(codes).to.include('E');",
|
||||
"});"
|
||||
], "type": "text/javascript" }}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user