fix(clients): stale signature bug, CKYC fixes, monthly turnover/annual income, senior citizen/PWD classification

- Add m_client.signature_id (single-slot pointer, mirrors image_id) plus
  ClientSignatureIdAdapter/EntitySignatureIdAdapter so re-uploading a signature updates the
  existing m_document row in place instead of leaving duplicate rows behind that readers picked
  inconsistently.
- Fix CkycBatchBuilderServiceImpl address-proof classification to read the client identifier's
  actual m_code (KYCAddressProofType vs KYCIdProofType) instead of a hardcoded string set that
  disagreed with the CKYC tab and silently dropped Aadhaar-as-address-proof identifiers.
- Organize the CKYC ZIP into per-artefact-type folders (PHOTO/SIGNATURE/ID_PROOF/ADDRESS_PROOF).
- Fix CkycBatchBuilderServiceImpl.createBatch() using the daily batch sequence number as the ZIP
  document's parent_entity_id instead of the real m_ckyc_batch.id, which 404'd on download
  whenever the two diverged (i.e. almost always).
- Add monthlyTurnover and annualIncome fields alongside the existing annualTurnover/monthlyIncome.
- Add isSeniorCitizen, isPwd, and pwdCategory (proper m_code_value-backed PwdCategory code, not a
  free-text column) to m_client, following the same pattern as occupation/religion/marital status.
- Add Diploma as its own Education Level (with diploma-specific DegreeQualification values) and
  Graduate as a level distinct from Undergraduate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 10:50:16 +05:30
co-authored by Claude Sonnet 5
parent bc30241950
commit 67214ec0f8
20 changed files with 771 additions and 45 deletions
@@ -441,6 +441,12 @@
<include file="parts/0418_add_user_agreement_permissions.xml" relativeToChangelogFile="true"/>
<include file="parts/0419_revert_first_login_gate_column_and_config.xml" relativeToChangelogFile="true"/>
<include file="parts/0420_add_missing_clientkyc_action_permissions.xml" relativeToChangelogFile="true"/>
<include file="parts/0421_add_signature_id_to_client.xml" relativeToChangelogFile="true"/>
<include file="parts/0422_add_monthly_turnover_to_client.xml" relativeToChangelogFile="true"/>
<include file="parts/0423_add_annual_income_to_client.xml" relativeToChangelogFile="true"/>
<include file="parts/0424_add_senior_citizen_pwd_to_client.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"/>
<!-- 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,73 @@
<?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">
<!--
m_client.image_id is a single-slot pointer into m_image: every photo upload overwrites the
same row, so every reader always sees the latest photo. The client signature had no
equivalent — every upload/re-crop created a brand-new m_document row named
'clientSignature' and never touched the previous one. Readers that don't explicitly
`order by id desc` (CKYC Documents tab, view-signature dialog, delete-signature dialog)
picked the FIRST matching row from an unordered query, i.e. the oldest signature, even
after the client re-cropped it — while CkycBatchBuilderServiceImpl (which does order by id
desc) correctly zipped up the latest one. This adds a signature_id pointer mirroring
image_id so signature storage becomes single-slot too, and backfills it from the latest
existing 'clientSignature' document per client so it starts out consistent with what the
CKYC ZIP builder already considered "current".
-->
<changeSet author="CBS" id="0421-1-add-signature-id-to-client">
<addColumn tableName="m_client">
<column name="signature_id" type="BIGINT">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
<changeSet author="CBS" id="0421-2-add-signature-id-index">
<createIndex indexName="FK_m_client_m_document_signature" tableName="m_client">
<column name="signature_id"/>
</createIndex>
</changeSet>
<changeSet author="CBS" id="0421-3-add-signature-id-fk">
<addForeignKeyConstraint baseColumnNames="signature_id" baseTableName="m_client"
constraintName="FK_m_client_m_document_signature" deferrable="false" initiallyDeferred="false"
onDelete="SET NULL" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_document" validate="true"/>
</changeSet>
<changeSet author="CBS" id="0421-4-backfill-signature-id">
<sql>
UPDATE m_client c
SET signature_id = (
SELECT MAX(d.id) FROM m_document d
WHERE d.parent_entity_type = 'clients' AND d.parent_entity_id = c.id AND d.name = 'clientSignature'
)
WHERE EXISTS (
SELECT 1 FROM m_document d
WHERE d.parent_entity_type = 'clients' AND d.parent_entity_id = c.id AND d.name = 'clientSignature'
)
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,32 @@
<?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">
<!-- Mirrors 0238_add_annual_turnover.xml — "Annual Turnover (Last FY)" is now labelled
"Yearly Turnover" in the client form, alongside this new "Monthly Turnover" field. -->
<changeSet author="CBS" id="0422-1">
<addColumn tableName="m_client">
<column name="monthly_turnover" type="DECIMAL(19,2)" defaultValue="null"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,32 @@
<?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">
<!-- Pairs with monthly_income the same way monthly_turnover pairs with annual_turnover
(0422_add_monthly_turnover_to_client.xml) — "Annual Income" alongside "Monthly Income". -->
<changeSet author="CBS" id="0423-1">
<addColumn tableName="m_client">
<column name="annual_income" type="DECIMAL(19,2)" defaultValue="null"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,111 @@
<?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">
<!-- Senior Citizen / Persons-with-Disability flags for RBI-mandated accessible-banking
provisions (doorstep banking eligibility for senior citizens 70+ and differently-abled/
infirm customers, per RBI's customer service circulars). isSeniorCitizen is auto-detected
client-side from Date of Birth (not user-editable); PWD is a manual flag. -->
<changeSet author="CBS" id="0424-1">
<addColumn tableName="m_client">
<column name="is_senior_citizen" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="is_pwd" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<!-- pwd_category_cv_id follows the same m_code_value FK pattern as occupation_category_cv_id /
marital_status_cv_id / religion_cv_id etc — a fixed free-text column would need a Java
change every time a new disability category is needed; a code value lets an admin add,
reorder, or deactivate categories from System -> Manage Codes with zero code changes. -->
<changeSet author="CBS" id="0424-2">
<addColumn tableName="m_client">
<column name="pwd_category_cv_id" type="BIGINT">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
<changeSet author="CBS" id="0424-3">
<createIndex indexName="FK_m_client_pwd_category_cv_id" tableName="m_client">
<column name="pwd_category_cv_id"/>
</createIndex>
</changeSet>
<changeSet author="CBS" id="0424-4">
<addForeignKeyConstraint baseColumnNames="pwd_category_cv_id" baseTableName="m_client"
constraintName="FK_m_client_pwd_category_cv_id" deferrable="false" initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_code_value" validate="true"/>
</changeSet>
<!-- ==================== PwdCategory code ====================
Categories per the Rights of Persons with Disabilities Act, 2016 (RPWD Act), the same
classification referenced by RBI's accessible-banking circulars. -->
<changeSet author="CBS" id="0424-5">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code WHERE code_name = 'PwdCategory'</sqlCheck>
</preConditions>
<insert tableName="m_code">
<column name="code_name" value="PwdCategory"/>
<column name="is_system_defined" valueBoolean="true"/>
</insert>
</changeSet>
<changeSet author="CBS" id="0424-6">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value WHERE code_id = (SELECT id FROM m_code WHERE code_name = 'PwdCategory')</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory)
SELECT id, val, val, pos, true, false FROM m_code
CROSS JOIN (VALUES
('Blindness', 1),
('Low Vision', 2),
('Leprosy Cured Persons', 3),
('Hearing Impairment (Deaf and Hard of Hearing)', 4),
('Locomotor Disability', 5),
('Dwarfism', 6),
('Intellectual Disability', 7),
('Mental Illness', 8),
('Autism Spectrum Disorder', 9),
('Cerebral Palsy', 10),
('Muscular Dystrophy', 11),
('Chronic Neurological Conditions', 12),
('Specific Learning Disabilities', 13),
('Multiple Sclerosis', 14),
('Speech and Language Disability', 15),
('Thalassemia', 16),
('Hemophilia', 17),
('Sickle Cell Disease', 18),
('Multiple Disabilities', 19),
('Acid Attack Victim', 20),
('Parkinson''s Disease', 21)
) AS t(val, pos)
WHERE code_name = 'PwdCategory';
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,83 @@
<?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">
<!-- "Diploma" becomes its own Education Level (alongside School / Undergraduate / Postgraduate
/ Doctorate) rather than a single generic entry nested under either of those — polytechnic
diploma holders are neither "School" nor "Undergraduate" in the strict sense. The
"Degree / Qualification" dropdown then offers diploma-specific branches when this level
is selected. -->
<changeSet id="0425_01_insert_diploma_education_level" author="CBS">
<sql>
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
SELECT c.id, 'Diploma',
(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 = 'EducationLevel'
AND NOT EXISTS (
SELECT 1 FROM m_code_value cv WHERE cv.code_id = c.id AND cv.code_value = 'Diploma'
);
</sql>
</changeSet>
<changeSet id="0425_02_insert_diploma_degree_values" author="CBS">
<sql>
INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory)
SELECT c.id, v.code_value, 'Diploma',
(SELECT COALESCE(MAX(cv.order_position), 0) FROM m_code_value cv WHERE cv.code_id = c.id) + v.rn,
true, false
FROM m_code c
CROSS JOIN (
VALUES
('Diploma in Mechanical Engineering', 1),
('Diploma in Civil Engineering', 2),
('Diploma in Electrical Engineering', 3),
('Diploma in Electronics &amp; Communication', 4),
('Diploma in Computer Science / IT', 5),
('Diploma in Automobile Engineering', 6),
('ITI Certificate', 7),
('Polytechnic Diploma (Other)', 8),
('Other Diploma', 9)
) AS v(code_value, rn)
WHERE c.code_name = 'DegreeQualification'
AND NOT EXISTS (
SELECT 1 FROM m_code_value cv
WHERE cv.code_id = c.id AND cv.code_value = v.code_value AND cv.code_description = 'Diploma'
);
</sql>
</changeSet>
<!-- The original "Diploma" DegreeQualification row (0256_add_education_level_degree_codes.xml)
was tagged under School — it's superseded by the standalone Diploma Education Level above,
so deactivate rather than delete (existing client records referencing it must keep resolving). -->
<changeSet id="0425_03_deactivate_school_diploma_degree_value" author="CBS">
<sql>
UPDATE m_code_value cv
SET is_active = false
WHERE cv.code_id = (SELECT id FROM m_code WHERE code_name = 'DegreeQualification')
AND cv.code_value = 'Diploma'
AND cv.code_description = 'School';
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,45 @@
<?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">
<!-- "Undergraduate" means currently pursuing a bachelor's degree (not yet completed);
"Graduate" means it has been completed. These are distinct statuses, so Graduate gets
its own Education Level rather than being folded into Undergraduate.
No frontend change needed for degree filtering: getEducationLevelCategory() in
client-general-step.component.ts already matches any name containing "graduate"
(Undergraduate OR Graduate) to the same 'undergraduate' bucket, so both levels correctly
show the same specific-degree list (B.Tech, B.A., B.Sc., ..., Other UG). -->
<changeSet id="0426_01_insert_graduate_education_level" author="CBS">
<sql>
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
SELECT c.id, 'Graduate',
(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 = 'EducationLevel'
AND NOT EXISTS (
SELECT 1 FROM m_code_value cv WHERE cv.code_id = c.id AND cv.code_value = 'Graduate'
);
</sql>
</changeSet>
</databaseChangeLog>