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:
Aditya
2026-04-28 16:25:41 +05:30
co-authored by Claude Opus 4.6
parent 9fb43ad971
commit 43b1e1da33
6 changed files with 1452 additions and 0 deletions
@@ -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();
}
}
@@ -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();
}
@@ -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/0256_add_education_level_degree_codes.xml" relativeToChangelogFile="true"/>
<include file="parts/0257_add_religion_social_category_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/0258_add_agriculture_non_agriculture_income.xml" relativeToChangelogFile="true"/>
<include file="parts/0259_add_ckyc_report_indexes.xml" relativeToChangelogFile="true"/>
</databaseChangeLog> </databaseChangeLog>
@@ -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" }}]
}
]
}
]
}