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
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:
@@ -406,6 +406,7 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
data.degreeQualificationOptions = templateData.degreeQualificationOptions;
|
data.degreeQualificationOptions = templateData.degreeQualificationOptions;
|
||||||
data.religionOptions = templateData.religionOptions;
|
data.religionOptions = templateData.religionOptions;
|
||||||
data.socialCategoryOptions = templateData.socialCategoryOptions;
|
data.socialCategoryOptions = templateData.socialCategoryOptions;
|
||||||
|
data.pwdCategoryOptions = templateData.pwdCategoryOptions;
|
||||||
data.maritalStatus = clientData.maritalStatus;
|
data.maritalStatus = clientData.maritalStatus;
|
||||||
data.educationLevel = clientData.educationLevel;
|
data.educationLevel = clientData.educationLevel;
|
||||||
data.degreeQualification = clientData.degreeQualification;
|
data.degreeQualification = clientData.degreeQualification;
|
||||||
@@ -467,6 +468,7 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
data.degreeQualificationOptions = clientData.degreeQualificationOptions;
|
data.degreeQualificationOptions = clientData.degreeQualificationOptions;
|
||||||
data.religionOptions = clientData.religionOptions;
|
data.religionOptions = clientData.religionOptions;
|
||||||
data.socialCategoryOptions = clientData.socialCategoryOptions;
|
data.socialCategoryOptions = clientData.socialCategoryOptions;
|
||||||
|
data.pwdCategoryOptions = clientData.pwdCategoryOptions;
|
||||||
data.maritalStatus = clientData.maritalStatus;
|
data.maritalStatus = clientData.maritalStatus;
|
||||||
data.educationLevel = clientData.educationLevel;
|
data.educationLevel = clientData.educationLevel;
|
||||||
data.degreeQualification = clientData.degreeQualification;
|
data.degreeQualification = clientData.degreeQualification;
|
||||||
|
|||||||
+2
-2
@@ -160,10 +160,10 @@ public class ClientKycApiResource {
|
|||||||
template.put("clientTypeName", clientTypeName);
|
template.put("clientTypeName", clientTypeName);
|
||||||
template.put("legalFormId", legalFormId);
|
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.
|
// served here stays consistent with what the UI computes from the same date of birth.
|
||||||
final LocalDate dateOfBirth = client.dateOfBirth();
|
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("isMinor", isMinor);
|
||||||
|
|
||||||
template.put("requiredDocuments", getRequiredDocuments(clientTypeName, legalFormId, isMinor));
|
template.put("requiredDocuments", getRequiredDocuments(clientTypeName, legalFormId, isMinor));
|
||||||
|
|||||||
+19
-7
@@ -71,6 +71,15 @@ public class ClientKycCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void validateForInitiate() {
|
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 List<ApiParameterError> errors = new ArrayList<>();
|
||||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ClientKycApiConstants.RESOURCE_NAME);
|
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");
|
"GSTIN must be a valid 15-character GST Identification Number");
|
||||||
}
|
}
|
||||||
|
|
||||||
// At least one ID document must be provided
|
// At least one ID document must be provided — except for minors, who prove identity via
|
||||||
final boolean hasPan = panNumber != null && !panNumber.isBlank();
|
// the Birth Certificate / School ID document-type picker instead.
|
||||||
final boolean hasAadhaar = aadhaarNumberMasked != null && !aadhaarNumberMasked.isBlank();
|
if (!skipIdentityDocumentCheck) {
|
||||||
final boolean hasGstin = gstinNumber != null && !gstinNumber.isBlank();
|
final boolean hasPan = panNumber != null && !panNumber.isBlank();
|
||||||
if (!hasPan && !hasAadhaar && !hasGstin) {
|
final boolean hasAadhaar = aadhaarNumberMasked != null && !aadhaarNumberMasked.isBlank();
|
||||||
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber)
|
final boolean hasGstin = gstinNumber != null && !gstinNumber.isBlank();
|
||||||
.notBlank();
|
if (!hasPan && !hasAadhaar && !hasGstin) {
|
||||||
|
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber)
|
||||||
|
.notBlank();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throwExceptionIfValidationWarningsExist(errors);
|
throwExceptionIfValidationWarningsExist(errors);
|
||||||
|
|||||||
+7
-3
@@ -84,9 +84,13 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
|||||||
this.context.authenticatedUser();
|
this.context.authenticatedUser();
|
||||||
LOG.info("initiateKyc: clientId={}, requestJson={}", clientId, command.json());
|
LOG.info("initiateKyc: clientId={}, requestJson={}", clientId, command.json());
|
||||||
|
|
||||||
|
final Client clientForValidation = clientRepository.findOneWithNotFoundDetection(clientId);
|
||||||
|
|
||||||
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
||||||
command.json(), ClientKycApiConstants.INITIATE_REQUEST_PARAMS);
|
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
|
// CIN / GSTIN are only applicable for NON_INDIVIDUAL clients
|
||||||
final Integer kycTypeOnInitiate = command.integerValueOfParameterNamed("kycType");
|
final Integer kycTypeOnInitiate = command.integerValueOfParameterNamed("kycType");
|
||||||
@@ -104,7 +108,7 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
|
final Client client = clientForValidation;
|
||||||
final ClientKyc kyc = ClientKyc.initiateKyc(client, command);
|
final ClientKyc kyc = ClientKyc.initiateKyc(client, command);
|
||||||
kycRepositoryWrapper.saveAndFlush(kyc);
|
kycRepositoryWrapper.saveAndFlush(kyc);
|
||||||
|
|
||||||
@@ -957,7 +961,7 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
|||||||
public void initiateKycInline(final Client client, final String kycJson) {
|
public void initiateKycInline(final Client client, final String kycJson) {
|
||||||
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
||||||
kycJson, ClientKycApiConstants.INITIATE_REQUEST_PARAMS);
|
kycJson, ClientKycApiConstants.INITIATE_REQUEST_PARAMS);
|
||||||
kycCommand.validateForInitiate();
|
kycCommand.validateForInitiate(Boolean.TRUE.equals(client.getIsMinor()));
|
||||||
|
|
||||||
if (kycRepositoryWrapper.existsByClientId(client.getId())) {
|
if (kycRepositoryWrapper.existsByClientId(client.getId())) {
|
||||||
LOG.info("KYC record already exists for client {}. Skipping inline initiation.", 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/0425_add_diploma_degree_qualification.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0426_add_graduate_education_level.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/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
|
<!-- 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
|
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
|
sequence independently. Liquibase tracks changesets by filename + id + author, not by the numeric
|
||||||
|
|||||||
+65
@@ -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>
|
||||||
Reference in New Issue
Block a user