feat(election): dynamic designations from m_code, email-based voting, vote remark field

- DesignationService reads designation limits and mandatory flags from DirectorDesignation m_code_value
- DirectorAppointmentServiceImpl: replaced hardcoded DESIGNATION_LIMITS map with dynamic DB fetch
- CommitteeServiceImpl: replaced hardcoded Chairman/Vice Chairman check with dynamic mandatory designation query
- New GET /v1/election/directors/designations endpoint returns all active designations
- Email-based voting module: MeetingPublicVoteApiResource, MeetingVoteAdminApiResource, MeetingVoteService
- MeetingMemberVote: added remark (TEXT) column for member vote remarks
- AgendaVoteRequest: added remark field to carry voice/typed remarks per agenda item
- Liquibase migrations 0354 (vote/resolution tables), 0355 (DirectorDesignation code), 0356 (vote remark column)
- SecurityConfig: permit public voting endpoints without authentication

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-07-07 18:26:14 +05:30
co-authored by Claude Sonnet 4.6
parent cfcd24efad
commit 934a51c9ee
30 changed files with 1196 additions and 1 deletions
@@ -687,3 +687,6 @@ spring.mail.properties.mail.transport.protocol=smtp
fineract.email.from-address=${FINERACT_EMAIL_FROM_ADDRESS:no-reply@cbsnexora.com}
fineract.email.from-name=${FINERACT_EMAIL_FROM_NAME:CBSnexora}
fineract.email.support-address=${FINERACT_EMAIL_SUPPORT_ADDRESS:support@cbsnexora.com}
# Frontend base URL — used for email links (voting, etc.)
cbs.frontend.base-url=${CBS_FRONTEND_BASE_URL:http://localhost:4200}
@@ -381,4 +381,7 @@
<include file="parts/0351_committee_meeting.xml" relativeToChangelogFile="true"/>
<include file="parts/0352_committee_meeting_rename_audit_cols.xml" relativeToChangelogFile="true"/>
<include file="parts/0353_committee_meeting_agenda.xml" relativeToChangelogFile="true"/>
<include file="parts/0354_meeting_vote_resolution.xml" relativeToChangelogFile="true"/>
<include file="parts/0355_director_designation_code.xml" relativeToChangelogFile="true"/>
<include file="parts/0356_meeting_vote_remark.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8"?>
<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.1.xsd">
<!-- Vote token: one per member per meeting, carries the secure email link -->
<changeSet id="0354-meeting-vote-token" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_election_meeting_vote_token"/></not>
</preConditions>
<createTable tableName="m_election_meeting_vote_token">
<column name="token" type="VARCHAR(36)">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="meeting_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="member_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="member_name" type="VARCHAR(200)">
<constraints nullable="false"/>
</column>
<column name="is_used" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="expires_at" type="TIMESTAMP">
<constraints nullable="false"/>
</column>
<column name="created_at" type="TIMESTAMP">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint
baseTableName="m_election_meeting_vote_token"
baseColumnNames="meeting_id"
referencedTableName="m_election_committee_meeting"
referencedColumnNames="id"
constraintName="fk_vote_token_meeting"
onDelete="CASCADE"/>
<createIndex tableName="m_election_meeting_vote_token" indexName="idx_vote_token_meeting_member">
<column name="meeting_id"/>
<column name="member_id"/>
</createIndex>
</changeSet>
<!-- Member vote: one row per member per agenda item -->
<changeSet id="0354-meeting-member-vote" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_election_meeting_member_vote"/></not>
</preConditions>
<createTable tableName="m_election_meeting_member_vote">
<column name="id" type="BIGSERIAL">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="meeting_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="agenda_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="member_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="vote" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="voted_at" type="TIMESTAMP">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint
baseTableName="m_election_meeting_member_vote"
baseColumnNames="meeting_id"
referencedTableName="m_election_committee_meeting"
referencedColumnNames="id"
constraintName="fk_member_vote_meeting"
onDelete="CASCADE"/>
<addForeignKeyConstraint
baseTableName="m_election_meeting_member_vote"
baseColumnNames="agenda_id"
referencedTableName="m_election_committee_meeting_agenda"
referencedColumnNames="id"
constraintName="fk_member_vote_agenda"
onDelete="CASCADE"/>
<addUniqueConstraint
tableName="m_election_meeting_member_vote"
columnNames="agenda_id, member_id"
constraintName="uq_member_vote_agenda_member"/>
<createIndex tableName="m_election_meeting_member_vote" indexName="idx_member_vote_meeting">
<column name="meeting_id"/>
<column name="member_id"/>
</createIndex>
</changeSet>
<!-- Agenda resolution: admin's final recorded decision per agenda item -->
<changeSet id="0354-meeting-agenda-resolution" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_election_meeting_agenda_resolution"/></not>
</preConditions>
<createTable tableName="m_election_meeting_agenda_resolution">
<column name="id" type="BIGSERIAL">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="meeting_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="agenda_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="resolution" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="discussion_notes" type="TEXT"/>
<column name="resolved_at" type="TIMESTAMP">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint
baseTableName="m_election_meeting_agenda_resolution"
baseColumnNames="meeting_id"
referencedTableName="m_election_committee_meeting"
referencedColumnNames="id"
constraintName="fk_resolution_meeting"
onDelete="CASCADE"/>
<addForeignKeyConstraint
baseTableName="m_election_meeting_agenda_resolution"
baseColumnNames="agenda_id"
referencedTableName="m_election_committee_meeting_agenda"
referencedColumnNames="id"
constraintName="fk_resolution_agenda"
onDelete="CASCADE"/>
<addUniqueConstraint
tableName="m_election_meeting_agenda_resolution"
columnNames="agenda_id"
constraintName="uq_resolution_agenda"/>
<createIndex tableName="m_election_meeting_agenda_resolution" indexName="idx_resolution_meeting">
<column name="meeting_id"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<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.1.xsd">
<changeSet id="0355-director-designation-code" author="CBS">
<sql>
INSERT INTO m_code (code_name, is_system_defined)
SELECT 'DirectorDesignation', false
WHERE NOT EXISTS (SELECT 1 FROM m_code WHERE code_name = 'DirectorDesignation');
</sql>
</changeSet>
<changeSet id="0355-director-designation-values" author="CBS">
<sql>
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory, code_score)
SELECT c.id, v.code_value, v.order_position, true, v.is_mandatory, v.code_score
FROM m_code c
CROSS JOIN (VALUES
('Chairman', 1, true, 1),
('Vice Chairman', 2, true, 1),
('Secretary', 3, false, NULL),
('Treasurer', 4, false, NULL),
('Director', 5, false, NULL),
('Principal Director', 6, false, 2)
) AS v(code_value, order_position, is_mandatory, code_score)
WHERE c.code_name = 'DirectorDesignation'
AND NOT EXISTS (
SELECT 1 FROM m_code_value cv
WHERE cv.code_id = c.id AND cv.code_value = v.code_value
);
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<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.1.xsd">
<changeSet id="0356-meeting-vote-remark" author="CBS">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_election_meeting_member_vote" columnName="remark"/></not>
</preConditions>
<addColumn tableName="m_election_meeting_member_vote">
<column name="remark" type="TEXT"/>
</addColumn>
</changeSet>
</databaseChangeLog>