From 43b1e1da33b021e1871d7aca9207c832a5d81b5f Mon Sep 17 00:00:00 2001 From: Aditya Date: Tue, 28 Apr 2026 16:25:41 +0530 Subject: [PATCH] Add CKYC Reports & Dashboard API module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../ckyc/api/CkycReportsApiResource.java | 220 +++++++ .../ckyc/service/CkycReportsReadService.java | 63 ++ .../service/CkycReportsReadServiceImpl.java | 605 ++++++++++++++++++ .../db/changelog/tenant/changelog-tenant.xml | 1 + .../parts/0259_add_ckyc_report_indexes.xml | 61 ++ ..._Reports_Dashboard.postman_collection.json | 502 +++++++++++++++ 6 files changed, 1452 insertions(+) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/client/ckyc/api/CkycReportsApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/client/ckyc/service/CkycReportsReadService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/client/ckyc/service/CkycReportsReadServiceImpl.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0259_add_ckyc_report_indexes.xml create mode 100644 postman/CKYC_Reports_Dashboard.postman_collection.json diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/ckyc/api/CkycReportsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/ckyc/api/CkycReportsApiResource.java new file mode 100644 index 000000000..0a68acabd --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/ckyc/api/CkycReportsApiResource.java @@ -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 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> 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> 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> 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 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> 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 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 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> listFiles() { + this.securityContext.authenticatedUser(); + return this.ckycReportsReadService.listUploadedFiles(); + } + + @POST + @Path("/files/generate") + @Operation(summary = "Generate outbound CKYC submission file") + public Response generateFile(Map request) { + this.securityContext.authenticatedUser(); + return this.ckycReportsReadService.generateCkycFile(request); + } + + // ===== 6. Master Data ===== + + @GET + @Path("/branches") + @Operation(summary = "List all branches/offices") + public List> getBranches() { + this.securityContext.authenticatedUser(); + return this.ckycReportsReadService.getBranches(); + } + + @GET + @Path("/states") + @Operation(summary = "List all states") + public List> getStates() { + this.securityContext.authenticatedUser(); + return this.ckycReportsReadService.getStates(); + } + + @GET + @Path("/codes/id-types") + @Operation(summary = "List KYC ID type codes") + public List> getIdTypes() { + this.securityContext.authenticatedUser(); + return this.ckycReportsReadService.getIdTypes(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/ckyc/service/CkycReportsReadService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/ckyc/service/CkycReportsReadService.java new file mode 100644 index 000000000..9da7a46d0 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/ckyc/service/CkycReportsReadService.java @@ -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 getSummary(Long branchId, String state, String dateFrom, String dateTo); + + List> getStateDistribution(Long branchId, String dateFrom, String dateTo); + + List> getBranchDistribution(Integer limit, String state, String dateFrom, String dateTo); + + List> getUploadTrend(String from, String to, Long branchId); + + // Customer list & search + Map listCustomers(String name, String branch, String city, String gender, String state, String dateFrom, String dateTo, + Integer page, Integer pageSize, String sort); + + List> searchCustomers(String type, String query); + + Map 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 parseAndPersistCkycFile(InputStream fileInputStream, FormDataContentDisposition fileDetail); + + List> listUploadedFiles(); + + Response generateCkycFile(Map request); + + // Master data + List> getBranches(); + + List> getStates(); + + List> getIdTypes(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/ckyc/service/CkycReportsReadServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/ckyc/service/CkycReportsReadServiceImpl.java new file mode 100644 index 000000000..12ff63362 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/ckyc/service/CkycReportsReadServiceImpl.java @@ -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> 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> 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 getSummary(Long branchId, String state, String dateFrom, String dateTo) { + StringBuilder sql = new StringBuilder(); + List 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 row = this.jdbcTemplate.queryForMap(sql.toString(), params.toArray()); + + // Duplicate mobile count + StringBuilder dupSql = new StringBuilder(); + List 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> getStateDistribution(Long branchId, String dateFrom, String dateTo) { + StringBuilder sql = new StringBuilder(); + List 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> getBranchDistribution(Integer limit, String state, String dateFrom, String dateTo) { + StringBuilder sql = new StringBuilder(); + List 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> getUploadTrend(String from, String to, Long branchId) { + StringBuilder sql = new StringBuilder(); + List 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 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 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 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> content = this.jdbcTemplate.queryForList(dataSql.toString(), dataParams.toArray()); + + Map 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> searchCustomers(String type, String query) { + if (query == null || query.trim().isEmpty()) { + return List.of(); + } + StringBuilder sql = new StringBuilder(); + List 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 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 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 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> 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 parseAndPersistCkycFile(InputStream fileInputStream, FormDataContentDisposition fileDetail) { + Map result = new LinkedHashMap<>(); + int totalRecords = 0; + int customers = 0; + int ids = 0; + int documents = 0; + List 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> 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 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> 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> getStates() { + List> result = new ArrayList<>(); + for (Map s : INDIAN_STATES) { + Map entry = new LinkedHashMap<>(); + entry.put("code", s.get("code")); + entry.put("name", s.get("name")); + result.add(entry); + } + return result; + } + + @Override + public List> getIdTypes() { + return ID_TYPES; + } + + // ===== Helpers ===== + + private void appendFilters(StringBuilder sql, List 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 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> rows) { + StringBuilder sb = new StringBuilder(); + sb.append("CIF,Full Name,Gender,DOB,Mobile,PAN,Aadhaar,Branch,Upload Date\n"); + for (Map 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> rows) { + StringBuilder sb = new StringBuilder(); + sb.append("10|CKYC_EXPORT|").append(LocalDate.now()).append("|").append(rows.size()).append("\n"); + for (Map 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() : ""; + } +} diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 6b48f3398..9cd5837fa 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -277,4 +277,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0259_add_ckyc_report_indexes.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0259_add_ckyc_report_indexes.xml new file mode 100644 index 000000000..8cc9c114f --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0259_add_ckyc_report_indexes.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/postman/CKYC_Reports_Dashboard.postman_collection.json b/postman/CKYC_Reports_Dashboard.postman_collection.json new file mode 100644 index 000000000..73dddbca0 --- /dev/null +++ b/postman/CKYC_Reports_Dashboard.postman_collection.json @@ -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" }}] + } + ] + } + ] +}