fix(clients): pwdCategoryOptions on edit template, minor-age threshold to 18, minor KYC identity-proof bypass
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s

- ClientData.templateOnTop()/templateWithSavingAccountOptions() never copied pwdCategoryOptions
  from the template — the Disability Type dropdown was always empty on the edit-mode GET
  /clients/{id}?template=true call, even though create-time template() had it.
- Raise the minor-detection threshold in ClientKycApiResource from age <10 to <18 (legal
  definition), mirroring the frontend's own rule so the required-documents list served here
  stays consistent.
- ClientKycCommand.validateForInitiate() unconditionally required at least one of
  PAN/Aadhaar/GSTIN, with no awareness of the Birth Certificate/School ID document-type picker
  minors actually use — blocking "Save & Next" on the KYC tab for every minor client. Added a
  skipIdentityDocumentCheck flag, set from client.getIsMinor() at both call sites
  (initiateKyc/initiateKycInline).
- Added Birth Certificate and School ID Card as KYCIdProofType code values — they were already
  offered as document-type options for minors but never existed in the code-value list that
  saveKycDocProof() resolves against, so selecting either silently failed to save any
  identifier/document.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 15:51:08 +05:30
co-authored by Claude Sonnet 5
parent aabda18b69
commit 3c58bb231f
6 changed files with 96 additions and 12 deletions
@@ -406,6 +406,7 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
data.degreeQualificationOptions = templateData.degreeQualificationOptions;
data.religionOptions = templateData.religionOptions;
data.socialCategoryOptions = templateData.socialCategoryOptions;
data.pwdCategoryOptions = templateData.pwdCategoryOptions;
data.maritalStatus = clientData.maritalStatus;
data.educationLevel = clientData.educationLevel;
data.degreeQualification = clientData.degreeQualification;
@@ -467,6 +468,7 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
data.degreeQualificationOptions = clientData.degreeQualificationOptions;
data.religionOptions = clientData.religionOptions;
data.socialCategoryOptions = clientData.socialCategoryOptions;
data.pwdCategoryOptions = clientData.pwdCategoryOptions;
data.maritalStatus = clientData.maritalStatus;
data.educationLevel = clientData.educationLevel;
data.degreeQualification = clientData.degreeQualification;
@@ -160,10 +160,10 @@ public class ClientKycApiResource {
template.put("clientTypeName", clientTypeName);
template.put("legalFormId", legalFormId);
// Minor detection mirrors the frontend's own rule (age < 10) so the document list
// Minor detection mirrors the frontend's own rule (age < 18) so the document list
// served here stays consistent with what the UI computes from the same date of birth.
final LocalDate dateOfBirth = client.dateOfBirth();
final boolean isMinor = dateOfBirth != null && Period.between(dateOfBirth, LocalDate.now()).getYears() < 10;
final boolean isMinor = dateOfBirth != null && Period.between(dateOfBirth, LocalDate.now()).getYears() < 18;
template.put("isMinor", isMinor);
template.put("requiredDocuments", getRequiredDocuments(clientTypeName, legalFormId, isMinor));
@@ -71,6 +71,15 @@ public class ClientKycCommand {
}
public void validateForInitiate() {
validateForInitiate(false);
}
/**
* @param skipIdentityDocumentCheck true for minors, who identify via the Birth Certificate /
* School ID document-type picker (a separate upload path)
* rather than PAN/Aadhaar/GSTIN.
*/
public void validateForInitiate(final boolean skipIdentityDocumentCheck) {
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ClientKycApiConstants.RESOURCE_NAME);
@@ -101,13 +110,16 @@ public class ClientKycCommand {
"GSTIN must be a valid 15-character GST Identification Number");
}
// At least one ID document must be provided
final boolean hasPan = panNumber != null && !panNumber.isBlank();
final boolean hasAadhaar = aadhaarNumberMasked != null && !aadhaarNumberMasked.isBlank();
final boolean hasGstin = gstinNumber != null && !gstinNumber.isBlank();
if (!hasPan && !hasAadhaar && !hasGstin) {
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber)
.notBlank();
// At least one ID document must be provided — except for minors, who prove identity via
// the Birth Certificate / School ID document-type picker instead.
if (!skipIdentityDocumentCheck) {
final boolean hasPan = panNumber != null && !panNumber.isBlank();
final boolean hasAadhaar = aadhaarNumberMasked != null && !aadhaarNumberMasked.isBlank();
final boolean hasGstin = gstinNumber != null && !gstinNumber.isBlank();
if (!hasPan && !hasAadhaar && !hasGstin) {
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber)
.notBlank();
}
}
throwExceptionIfValidationWarningsExist(errors);
@@ -84,9 +84,13 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
this.context.authenticatedUser();
LOG.info("initiateKyc: clientId={}, requestJson={}", clientId, command.json());
final Client clientForValidation = clientRepository.findOneWithNotFoundDetection(clientId);
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
command.json(), ClientKycApiConstants.INITIATE_REQUEST_PARAMS);
kycCommand.validateForInitiate();
// Minors identify via the Birth Certificate / School ID document-type picker (a separate
// upload path), not PAN/Aadhaar/GSTIN — so the "at least one ID" check doesn't apply to them.
kycCommand.validateForInitiate(Boolean.TRUE.equals(clientForValidation.getIsMinor()));
// CIN / GSTIN are only applicable for NON_INDIVIDUAL clients
final Integer kycTypeOnInitiate = command.integerValueOfParameterNamed("kycType");
@@ -104,7 +108,7 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
}
try {
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
final Client client = clientForValidation;
final ClientKyc kyc = ClientKyc.initiateKyc(client, command);
kycRepositoryWrapper.saveAndFlush(kyc);
@@ -957,7 +961,7 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
public void initiateKycInline(final Client client, final String kycJson) {
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
kycJson, ClientKycApiConstants.INITIATE_REQUEST_PARAMS);
kycCommand.validateForInitiate();
kycCommand.validateForInitiate(Boolean.TRUE.equals(client.getIsMinor()));
if (kycRepositoryWrapper.existsByClientId(client.getId())) {
LOG.info("KYC record already exists for client {}. Skipping inline initiation.", client.getId());
@@ -448,6 +448,7 @@
<include file="parts/0425_add_diploma_degree_qualification.xml" relativeToChangelogFile="true"/>
<include file="parts/0426_add_graduate_education_level.xml" relativeToChangelogFile="true"/>
<include file="parts/0427_add_caretaker_relationship_value.xml" relativeToChangelogFile="true"/>
<include file="parts/0428_add_minor_kyc_id_proof_codes.xml" relativeToChangelogFile="true"/>
<!-- NOTE: the numbers below (0341-0359) re-use prefixes already used above by the chequebook/CTS
clearing catchup migrations — these come from a long-diverged branch (uate) that assigned its own
sequence independently. Liquibase tracks changesets by filename + id + author, not by the numeric
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<!-- ClientKycApiResource.getRequiredDocuments() has always offered "Birth Certificate" and
"School ID Card" as identity-proof options for minor clients, but neither was ever added
as a KYCIdProofType code value (0251_add_kyc_proof_type_codes.xml). Since
edit-client/create-client's saveKycDocProof() resolves the picked document type against
this code list (resolveDocumentTypeId), selecting either option for a minor silently
failed to save any identifier/document — the KYC record's own document-name/number fields
persisted fine, but the uploaded file never did. -->
<changeSet author="CBS" id="0428-1">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">
SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON c.id = cv.code_id
WHERE c.code_name = 'KYCIdProofType' AND cv.code_value = 'Birth Certificate'
</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory)
SELECT c.id, 'Birth Certificate', 'BIRTH_CERTIFICATE',
(SELECT COALESCE(MAX(cv.order_position), 0) FROM m_code_value cv WHERE cv.code_id = c.id) + 1,
true, false
FROM m_code c
WHERE c.code_name = 'KYCIdProofType';
</sql>
</changeSet>
<changeSet author="CBS" id="0428-2">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">
SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON c.id = cv.code_id
WHERE c.code_name = 'KYCIdProofType' AND cv.code_value = 'School ID Card'
</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory)
SELECT c.id, 'School ID Card', 'SCHOOL_ID_CARD',
(SELECT COALESCE(MAX(cv.order_position), 0) FROM m_code_value cv WHERE cv.code_id = c.id) + 1,
true, false
FROM m_code c
WHERE c.code_name = 'KYCIdProofType';
</sql>
</changeSet>
</databaseChangeLog>