feat(election): add election module — config, nominations, candidates, voting, approval flow

- REST API resources: ElectionConfig, ElectionNomination, ElectionCandidate, ElectionVoting
- Domain entities, repositories, services (read + write) with maker-checker approval steps
- Eligibility checks, no-due-certificate, CEO pre-check services
- Liquibase migration 0337 creates all election module tables

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-06-19 17:39:43 +05:30
co-authored by Claude Sonnet 4.6
parent c1e23080fc
commit 5899d2ca07
60 changed files with 3162 additions and 0 deletions
@@ -0,0 +1,65 @@
package org.apache.fineract.portfolio.election.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.DELETE;
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 lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.election.service.ElectionReadPlatformService;
import org.apache.fineract.portfolio.election.service.ElectionWritePlatformService;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@Path("/v1/election")
@Tag(name = "Election Candidates", description = "Manage approved election candidates")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@RequiredArgsConstructor
public class ElectionCandidateApiResource {
private final ElectionReadPlatformService readService;
private final ElectionWritePlatformService writeService;
private final PlatformSecurityContext context;
@GET
@Path("/{electionId}/candidates")
@Operation(summary = "Get all approved candidates for an election")
public Response getCandidates(@PathParam("electionId") Long electionId,
@QueryParam("postId") Long postId) {
if (postId != null) {
return Response.ok(readService.retrieveCandidatesByPost(electionId, postId)).build();
}
return Response.ok(readService.retrieveCandidatesByElection(electionId)).build();
}
@POST
@Path("/{electionId}/nomination/{nominationId}/approve-candidate")
@Operation(summary = "Approve nomination and add to candidates list")
public Response approveCandidate(@PathParam("electionId") Long electionId,
@PathParam("nominationId") Long nominationId) {
Long approvedBy = context.authenticatedUser().getId();
Long candidateId = writeService.approveCandidate(nominationId, approvedBy);
return Response.ok(Map.of("resourceId", candidateId)).build();
}
@DELETE
@Path("/{electionId}/candidate/{candidateId}")
@Operation(summary = "Remove a candidate from the election")
public Response removeCandidate(@PathParam("electionId") Long electionId,
@PathParam("candidateId") Long candidateId,
@QueryParam("reason") String reason) {
writeService.removeCandidate(candidateId, reason);
return Response.ok(Map.of("resourceId", candidateId, "status", "REMOVED")).build();
}
}
@@ -0,0 +1,106 @@
package org.apache.fineract.portfolio.election.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
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 lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.election.data.ElectionConfigRequest;
import org.apache.fineract.portfolio.election.service.ElectionReadPlatformService;
import org.apache.fineract.portfolio.election.service.ElectionWritePlatformService;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@Path("/v1/election/config")
@Tag(name = "Election Configuration", description = "Manage cooperative election setup")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@RequiredArgsConstructor
public class ElectionConfigApiResource {
private final ElectionReadPlatformService readService;
private final ElectionWritePlatformService writeService;
private final ObjectMapper objectMapper;
@GET
@Operation(summary = "List all elections")
public Response listElections() {
return Response.ok(readService.retrieveAllElections()).build();
}
@GET
@Path("/{electionId}")
@Operation(summary = "Get election details")
public Response getElection(@PathParam("electionId") Long electionId) {
return Response.ok(readService.retrieveElection(electionId)).build();
}
@POST
@Operation(summary = "Create a new election")
public Response createElection(ElectionConfigRequest request) {
Long id = writeService.createElection(request);
return Response.ok(Map.of("resourceId", id)).build();
}
@PUT
@Path("/{electionId}")
@Operation(summary = "Update election configuration")
public Response updateElection(@PathParam("electionId") Long electionId,
ElectionConfigRequest request) {
writeService.updateElection(electionId, request);
return Response.ok(Map.of("resourceId", electionId)).build();
}
@POST
@Path("/{electionId}/activate")
@Operation(summary = "Activate election to open nominations")
public Response activateElection(@PathParam("electionId") Long electionId) {
writeService.activateElection(electionId);
return Response.ok(Map.of("resourceId", electionId, "status", "ACTIVE")).build();
}
@POST
@Path("/{electionId}/close-nominations")
@Operation(summary = "Close nomination window")
public Response closeNominations(@PathParam("electionId") Long electionId) {
writeService.closeNominations(electionId);
return Response.ok(Map.of("resourceId", electionId, "status", "NOMINATION_CLOSED")).build();
}
@POST
@Path("/{electionId}/open-voting")
@Operation(summary = "Open voting")
public Response openVoting(@PathParam("electionId") Long electionId) {
writeService.openVoting(electionId);
return Response.ok(Map.of("resourceId", electionId, "status", "VOTING_OPEN")).build();
}
@POST
@Path("/{electionId}/close-voting")
@Operation(summary = "Close voting")
public Response closeVoting(@PathParam("electionId") Long electionId) {
writeService.closeVoting(electionId);
return Response.ok(Map.of("resourceId", electionId, "status", "VOTING_CLOSED")).build();
}
@POST
@Path("/{electionId}/cancel")
@Operation(summary = "Cancel election")
public Response cancelElection(@PathParam("electionId") Long electionId,
@QueryParam("reason") String reason) {
writeService.cancelElection(electionId, reason);
return Response.ok(Map.of("resourceId", electionId, "status", "CANCELLED")).build();
}
}
@@ -0,0 +1,112 @@
package org.apache.fineract.portfolio.election.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.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 lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.election.data.ElectionApprovalActionRequest;
import org.apache.fineract.portfolio.election.data.ElectionNominationRequest;
import org.apache.fineract.portfolio.election.service.ElectionReadPlatformService;
import org.apache.fineract.portfolio.election.service.ElectionWritePlatformService;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@Path("/v1/election")
@Tag(name = "Election Nomination", description = "Manage election nominations and approval workflow")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@RequiredArgsConstructor
public class ElectionNominationApiResource {
private final ElectionReadPlatformService readService;
private final ElectionWritePlatformService writeService;
@POST
@Path("/{electionId}/nomination")
@Operation(summary = "Submit a nomination for election")
public Response submitNomination(@PathParam("electionId") Long electionId,
ElectionNominationRequest request) {
request.setElectionId(electionId);
Long id = writeService.submitNomination(request);
return Response.ok(Map.of("resourceId", id)).build();
}
@GET
@Path("/{electionId}/nominations")
@Operation(summary = "List all nominations for an election")
public Response listNominations(@PathParam("electionId") Long electionId,
@QueryParam("status") String status) {
return Response.ok(readService.retrieveNominationsByElection(electionId, status)).build();
}
@GET
@Path("/{electionId}/nomination/{nominationId}")
@Operation(summary = "Get nomination details with eligibility and CEO check")
public Response getNomination(@PathParam("electionId") Long electionId,
@PathParam("nominationId") Long nominationId) {
return Response.ok(readService.retrieveNomination(nominationId)).build();
}
@POST
@Path("/{electionId}/nomination/{nominationId}/withdraw")
@Operation(summary = "Withdraw nomination")
public Response withdrawNomination(@PathParam("electionId") Long electionId,
@PathParam("nominationId") Long nominationId) {
writeService.withdrawNomination(nominationId);
return Response.ok(Map.of("resourceId", nominationId, "status", "WITHDRAWN")).build();
}
@POST
@Path("/{electionId}/nomination/{nominationId}/eligibility-check")
@Operation(summary = "Run automated eligibility check (age, gender, caste, shares, area)")
public Response runEligibilityCheck(@PathParam("electionId") Long electionId,
@PathParam("nominationId") Long nominationId) {
writeService.runEligibilityCheck(nominationId);
return Response.ok(readService.retrieveNomination(nominationId)).build();
}
@POST
@Path("/{electionId}/nomination/{nominationId}/ceo-precheck")
@Operation(summary = "Run CEO pre-check (director loan, relation accounts)")
public Response runCeoPreCheck(@PathParam("electionId") Long electionId,
@PathParam("nominationId") Long nominationId) {
writeService.runCeoPreCheck(nominationId);
return Response.ok(readService.retrieveNomination(nominationId)).build();
}
@POST
@Path("/{electionId}/nomination/{nominationId}/no-due-certificate")
@Operation(summary = "Generate No Due Certificate for nominee")
public Response generateNoDueCertificate(@PathParam("electionId") Long electionId,
@PathParam("nominationId") Long nominationId) {
String certNumber = writeService.generateNoDueCertificate(nominationId);
return Response.ok(Map.of("certificateNumber", certNumber)).build();
}
@POST
@Path("/{electionId}/nomination/{nominationId}/approval-action")
@Operation(summary = "Process approval step (approve or reject)")
public Response processApprovalStep(@PathParam("electionId") Long electionId,
@PathParam("nominationId") Long nominationId,
ElectionApprovalActionRequest request) {
writeService.processApprovalStep(nominationId, request);
return Response.ok(readService.retrieveNomination(nominationId)).build();
}
@GET
@Path("/nominations/member/{memberId}")
@Operation(summary = "Get all nominations submitted by a member")
public Response getNominationsByMember(@PathParam("memberId") Long memberId) {
return Response.ok(readService.retrieveNominationsByMember(memberId)).build();
}
}
@@ -0,0 +1,60 @@
package org.apache.fineract.portfolio.election.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.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 lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.election.data.ElectionVoteRequest;
import org.apache.fineract.portfolio.election.service.ElectionReadPlatformService;
import org.apache.fineract.portfolio.election.service.ElectionWritePlatformService;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@Path("/v1/election")
@Tag(name = "Election Voting", description = "Election day voting and results")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@RequiredArgsConstructor
public class ElectionVotingApiResource {
private final ElectionReadPlatformService readService;
private final ElectionWritePlatformService writeService;
private final PlatformSecurityContext context;
@POST
@Path("/{electionId}/vote")
@Operation(summary = "Cast a vote for a candidate")
public Response castVote(@PathParam("electionId") Long electionId,
ElectionVoteRequest request) {
request.setElectionId(electionId);
writeService.castVote(request);
return Response.ok(Map.of("message", "Vote cast successfully")).build();
}
@POST
@Path("/{electionId}/declare-results")
@Operation(summary = "Declare election results")
public Response declareResults(@PathParam("electionId") Long electionId) {
Long declaredBy = context.authenticatedUser().getId();
writeService.declareResults(electionId, declaredBy);
return Response.ok(readService.retrieveResultsByElection(electionId)).build();
}
@GET
@Path("/{electionId}/results")
@Operation(summary = "Get election results")
public Response getResults(@PathParam("electionId") Long electionId) {
return Response.ok(readService.retrieveResultsByElection(electionId)).build();
}
}
@@ -0,0 +1,19 @@
package org.apache.fineract.portfolio.election.data;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class ElectionApprovalActionRequest {
private String action;
private String remarks;
private Boolean familyDetailsVerified;
private String familyDetailsJson;
private String rejectionReason;
}
@@ -0,0 +1,28 @@
package org.apache.fineract.portfolio.election.data;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ElectionApprovalStepData {
private Long id;
private Long nominationId;
private Integer stepNumber;
private String stepName;
private String approverRole;
private Long approverUserId;
private String approverName;
private String status;
private String remarks;
private LocalDateTime actionDate;
private Boolean familyDetailsVerified;
private String familyDetailsJson;
}
@@ -0,0 +1,33 @@
package org.apache.fineract.portfolio.election.data;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ElectionCandidateData {
private Long id;
private Long electionId;
private Long nominationId;
private Long postId;
private String postName;
private String candidateName;
private Integer serialNumber;
private String photoUrl;
private Boolean isActive;
private LocalDateTime approvedDate;
private String symbol;
private String briefProfile;
private String gender;
private String caste;
private String location;
private Integer shareCount;
private Integer voteCount;
}
@@ -0,0 +1,33 @@
package org.apache.fineract.portfolio.election.data;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ElectionCeoPreCheckData {
private Long id;
private Long nominationId;
private String directorLoanCheckResult;
private BigDecimal directorLoanOutstanding;
private String directorLoanRemarks;
private String relationAcCheckResult;
private String relationAccountsDetails;
private String relationAcRemarks;
private String noDueCheckResult;
private String noDueCertificateNumber;
private LocalDateTime noDueGeneratedDate;
private String overallResult;
private Long checkedBy;
private String checkedByName;
private LocalDateTime checkedDate;
private String remarks;
}
@@ -0,0 +1,32 @@
package org.apache.fineract.portfolio.election.data;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ElectionConfigData {
private Long id;
private String title;
private String description;
private String electionType;
private LocalDate nominationStartDate;
private LocalDate nominationEndDate;
private LocalDate electionDate;
private LocalDate resultDate;
private String status;
private Boolean isActive;
private String remarks;
private List<ElectionPostData> posts;
private Long totalNominations;
private Long approvedNominations;
private Long pendingNominations;
}
@@ -0,0 +1,26 @@
package org.apache.fineract.portfolio.election.data;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class ElectionConfigRequest {
private String title;
private String description;
private String electionType;
private LocalDate nominationStartDate;
private LocalDate nominationEndDate;
private LocalDate electionDate;
private LocalDate resultDate;
private String remarks;
private List<ElectionPostRequest> posts;
}
@@ -0,0 +1,23 @@
package org.apache.fineract.portfolio.election.data;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ElectionEligibilityCheckData {
private Long id;
private Long nominationId;
private String overallResult;
private Long checkedBy;
private String checkedByName;
private LocalDateTime checkedDate;
private String remarks;
}
@@ -0,0 +1,48 @@
package org.apache.fineract.portfolio.election.data;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ElectionNominationData {
private Long id;
private Long electionId;
private String electionTitle;
private Long postId;
private String postName;
private Long memberId;
private Long shareAccountId;
private String applicationNumber;
private String applicantName;
private LocalDate dateOfBirth;
private Integer age;
private String gender;
private String caste;
private String casteCategory;
private String location;
private String serviceDetails;
private String mobileNumber;
private String email;
private String address;
private String occupation;
private Integer shareCount;
private String status;
private String statusDisplayName;
private String rejectionReason;
private LocalDateTime submittedDate;
private Boolean declarationAccepted;
private Long photoDocumentId;
private ElectionEligibilityCheckData eligibilityCheck;
private ElectionCeoPreCheckData ceoPreCheck;
private List<ElectionApprovalStepData> approvalSteps;
}
@@ -0,0 +1,34 @@
package org.apache.fineract.portfolio.election.data;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class ElectionNominationRequest {
private Long electionId;
private Long postId;
private Long memberId;
private Long shareAccountId;
private String applicantName;
private LocalDate dateOfBirth;
private String gender;
private String caste;
private String casteCategory;
private String location;
private String serviceDetails;
private String mobileNumber;
private String email;
private String address;
private String occupation;
private Integer shareCount;
private Boolean declarationAccepted;
private Long photoDocumentId;
}
@@ -0,0 +1,21 @@
package org.apache.fineract.portfolio.election.data;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ElectionPostData {
private Long id;
private Long electionId;
private String postName;
private Integer totalSeats;
private Boolean isActive;
private Long filledSeats;
private Long remainingSeats;
}
@@ -0,0 +1,16 @@
package org.apache.fineract.portfolio.election.data;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class ElectionPostRequest {
private String postName;
private Integer totalSeats;
}
@@ -0,0 +1,29 @@
package org.apache.fineract.portfolio.election.data;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ElectionResultData {
private Long id;
private Long electionId;
private String electionTitle;
private Long postId;
private String postName;
private Long candidateId;
private String candidateName;
private Integer voteCount;
private String result;
private Integer rankPosition;
private LocalDateTime declaredDate;
private List<ElectionResultData> postResults;
}
@@ -0,0 +1,19 @@
package org.apache.fineract.portfolio.election.data;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class ElectionVoteRequest {
private Long electionId;
private Long postId;
private Long voterMemberId;
private Long candidateId;
private String boothId;
}
@@ -0,0 +1,8 @@
package org.apache.fineract.portfolio.election.domain;
public enum ApprovalStepStatus {
PENDING,
APPROVED,
REJECTED,
SKIPPED
}
@@ -0,0 +1,8 @@
package org.apache.fineract.portfolio.election.domain;
public enum CheckResult {
PASS,
FAIL,
PENDING,
WAIVED
}
@@ -0,0 +1,77 @@
package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import java.time.LocalDateTime;
@Entity
@Table(name = "m_election_approval_step")
@Getter
@Setter
@NoArgsConstructor
public class ElectionApprovalStep extends AbstractPersistableCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "nomination_id", nullable = false)
private ElectionNomination nomination;
@Column(name = "step_number", nullable = false)
private Integer stepNumber;
@Column(name = "step_name", nullable = false, length = 200)
private String stepName;
@Column(name = "approver_role", length = 100)
private String approverRole;
@Column(name = "approver_user_id")
private Long approverUserId;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 50)
private ApprovalStepStatus status = ApprovalStepStatus.PENDING;
@Column(name = "remarks", columnDefinition = "TEXT")
private String remarks;
@Column(name = "action_date")
private LocalDateTime actionDate;
@Column(name = "family_details_verified", nullable = false)
private Boolean familyDetailsVerified = false;
@Column(name = "family_details_json", columnDefinition = "TEXT")
private String familyDetailsJson;
public static ElectionApprovalStep create(ElectionNomination nomination,
int stepNumber, String stepName, String approverRole) {
ElectionApprovalStep step = new ElectionApprovalStep();
step.nomination = nomination;
step.stepNumber = stepNumber;
step.stepName = stepName;
step.approverRole = approverRole;
step.status = ApprovalStepStatus.PENDING;
step.familyDetailsVerified = false;
return step;
}
public void approve(Long approverUserId, String remarks, boolean familyVerified, String familyJson) {
this.approverUserId = approverUserId;
this.status = ApprovalStepStatus.APPROVED;
this.remarks = remarks;
this.actionDate = LocalDateTime.now();
this.familyDetailsVerified = familyVerified;
this.familyDetailsJson = familyJson;
}
public void reject(Long approverUserId, String remarks) {
this.approverUserId = approverUserId;
this.status = ApprovalStepStatus.REJECTED;
this.remarks = remarks;
this.actionDate = LocalDateTime.now();
}
}
@@ -0,0 +1,23 @@
package org.apache.fineract.portfolio.election.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface ElectionApprovalStepRepository extends JpaRepository<ElectionApprovalStep, Long> {
List<ElectionApprovalStep> findByNominationIdOrderByStepNumberAsc(Long nominationId);
Optional<ElectionApprovalStep> findByNominationIdAndStepNumber(Long nominationId, int stepNumber);
@Query("SELECT s FROM ElectionApprovalStep s WHERE s.nomination.id = :nominationId " +
"AND s.status = org.apache.fineract.portfolio.election.domain.ApprovalStepStatus.PENDING ORDER BY s.stepNumber ASC")
Optional<ElectionApprovalStep> findNextPendingStep(@Param("nominationId") Long nominationId);
boolean existsByNominationIdAndStatus(Long nominationId, ApprovalStepStatus status);
}
@@ -0,0 +1,75 @@
package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import java.time.LocalDateTime;
@Entity
@Table(name = "m_election_candidate")
@Getter
@Setter
@NoArgsConstructor
public class ElectionCandidate extends AbstractPersistableCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "election_id", nullable = false)
private ElectionConfig electionConfig;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "nomination_id", nullable = false, unique = true)
private ElectionNomination nomination;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private ElectionPost electionPost;
@Column(name = "candidate_name", nullable = false, length = 200)
private String candidateName;
@Column(name = "serial_number")
private Integer serialNumber;
@Column(name = "photo_url", length = 500)
private String photoUrl;
@Column(name = "is_active", nullable = false)
private Boolean isActive = true;
@Column(name = "approved_by")
private Long approvedBy;
@Column(name = "approved_date")
private LocalDateTime approvedDate;
@Column(name = "symbol", length = 100)
private String symbol;
@Column(name = "brief_profile", columnDefinition = "TEXT")
private String briefProfile;
public static ElectionCandidate create(ElectionConfig electionConfig,
ElectionNomination nomination,
ElectionPost electionPost,
String candidateName,
Integer serialNumber,
Long approvedBy) {
ElectionCandidate candidate = new ElectionCandidate();
candidate.electionConfig = electionConfig;
candidate.nomination = nomination;
candidate.electionPost = electionPost;
candidate.candidateName = candidateName;
candidate.serialNumber = serialNumber;
candidate.approvedBy = approvedBy;
candidate.approvedDate = LocalDateTime.now();
candidate.isActive = true;
return candidate;
}
public void deactivate() {
this.isActive = false;
}
}
@@ -0,0 +1,29 @@
package org.apache.fineract.portfolio.election.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface ElectionCandidateRepository extends JpaRepository<ElectionCandidate, Long> {
List<ElectionCandidate> findByElectionConfigIdAndIsActiveTrue(Long electionId);
List<ElectionCandidate> findByElectionConfigIdAndElectionPostIdAndIsActiveTrue(
Long electionId, Long postId);
Optional<ElectionCandidate> findByNominationId(Long nominationId);
@Query("SELECT c FROM ElectionCandidate c WHERE c.electionConfig.id = :electionId " +
"AND c.isActive = true ORDER BY c.electionPost.postName, c.serialNumber")
List<ElectionCandidate> findAllActiveCandidates(@Param("electionId") Long electionId);
@Query("SELECT MAX(c.serialNumber) FROM ElectionCandidate c " +
"WHERE c.electionConfig.id = :electionId AND c.electionPost.id = :postId")
Optional<Integer> findMaxSerialNumberByElectionAndPost(
@Param("electionId") Long electionId, @Param("postId") Long postId);
}
@@ -0,0 +1,103 @@
package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "m_election_ceo_precheck")
@Getter
@Setter
@NoArgsConstructor
public class ElectionCeoPrecheck extends AbstractPersistableCustom<Long> {
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "nomination_id", nullable = false, unique = true)
private ElectionNomination nomination;
@Enumerated(EnumType.STRING)
@Column(name = "director_loan_check_result", length = 20)
private CheckResult directorLoanCheckResult = CheckResult.PENDING;
@Column(name = "director_loan_outstanding", precision = 19, scale = 4)
private BigDecimal directorLoanOutstanding = BigDecimal.ZERO;
@Column(name = "director_loan_remarks", columnDefinition = "TEXT")
private String directorLoanRemarks;
@Enumerated(EnumType.STRING)
@Column(name = "relation_ac_check_result", length = 20)
private CheckResult relationAcCheckResult = CheckResult.PENDING;
@Column(name = "relation_accounts_details", columnDefinition = "TEXT")
private String relationAccountsDetails;
@Column(name = "relation_ac_remarks", columnDefinition = "TEXT")
private String relationAcRemarks;
@Enumerated(EnumType.STRING)
@Column(name = "no_due_check_result", length = 20)
private CheckResult noDueCheckResult = CheckResult.PENDING;
@Column(name = "no_due_certificate_number", length = 100)
private String noDueCertificateNumber;
@Column(name = "no_due_generated_date")
private LocalDateTime noDueGeneratedDate;
@Enumerated(EnumType.STRING)
@Column(name = "overall_result", length = 20)
private CheckResult overallResult = CheckResult.PENDING;
@Column(name = "checked_by")
private Long checkedBy;
@Column(name = "checked_date")
private LocalDateTime checkedDate;
@Column(name = "remarks", columnDefinition = "TEXT")
private String remarks;
public static ElectionCeoPrecheck create(ElectionNomination nomination) {
ElectionCeoPrecheck precheck = new ElectionCeoPrecheck();
precheck.nomination = nomination;
precheck.overallResult = CheckResult.PENDING;
precheck.directorLoanCheckResult = CheckResult.PENDING;
precheck.relationAcCheckResult = CheckResult.PENDING;
precheck.noDueCheckResult = CheckResult.PENDING;
return precheck;
}
public void completeDirectorLoanCheck(CheckResult result, BigDecimal outstanding, String remarks) {
this.directorLoanCheckResult = result;
this.directorLoanOutstanding = outstanding != null ? outstanding : BigDecimal.ZERO;
this.directorLoanRemarks = remarks;
}
public void completeRelationAcCheck(CheckResult result, String relationDetails, String remarks) {
this.relationAcCheckResult = result;
this.relationAccountsDetails = relationDetails;
this.relationAcRemarks = remarks;
}
public void generateNoDueCertificate(String certificateNumber) {
this.noDueCertificateNumber = certificateNumber;
this.noDueGeneratedDate = LocalDateTime.now();
this.noDueCheckResult = CheckResult.PASS;
}
public void finalise(Long checkedBy, String remarks) {
this.checkedBy = checkedBy;
this.checkedDate = LocalDateTime.now();
this.remarks = remarks;
boolean allPass = directorLoanCheckResult == CheckResult.PASS
&& relationAcCheckResult == CheckResult.PASS
&& noDueCheckResult == CheckResult.PASS;
this.overallResult = allPass ? CheckResult.PASS : CheckResult.FAIL;
}
}
@@ -0,0 +1,12 @@
package org.apache.fineract.portfolio.election.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface ElectionCeoPrecheckRepository extends JpaRepository<ElectionCeoPrecheck, Long> {
Optional<ElectionCeoPrecheck> findByNominationId(Long nominationId);
}
@@ -0,0 +1,111 @@
package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "m_election_config")
@Getter
@Setter
@NoArgsConstructor
public class ElectionConfig extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "title", nullable = false, length = 200)
private String title;
@Column(name = "description", columnDefinition = "TEXT")
private String description;
@Enumerated(EnumType.STRING)
@Column(name = "election_type", nullable = false, length = 50)
private ElectionType electionType;
@Column(name = "nomination_start_date", nullable = false)
private LocalDate nominationStartDate;
@Column(name = "nomination_end_date", nullable = false)
private LocalDate nominationEndDate;
@Column(name = "election_date", nullable = false)
private LocalDate electionDate;
@Column(name = "result_date")
private LocalDate resultDate;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 50)
private ElectionStatus status = ElectionStatus.DRAFT;
@Column(name = "is_active", nullable = false)
private Boolean isActive = true;
@Column(name = "remarks", columnDefinition = "TEXT")
private String remarks;
@OneToMany(mappedBy = "electionConfig", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<ElectionPost> posts = new ArrayList<>();
private ElectionConfig(String title, String description, ElectionType electionType,
LocalDate nominationStartDate, LocalDate nominationEndDate,
LocalDate electionDate, LocalDate resultDate) {
this.title = title;
this.description = description;
this.electionType = electionType;
this.nominationStartDate = nominationStartDate;
this.nominationEndDate = nominationEndDate;
this.electionDate = electionDate;
this.resultDate = resultDate;
this.status = ElectionStatus.DRAFT;
this.isActive = true;
}
public static ElectionConfig create(String title, String description, ElectionType electionType,
LocalDate nominationStartDate, LocalDate nominationEndDate,
LocalDate electionDate, LocalDate resultDate) {
return new ElectionConfig(title, description, electionType, nominationStartDate,
nominationEndDate, electionDate, resultDate);
}
public boolean isNominationOpen() {
LocalDate today = LocalDate.now();
return status == ElectionStatus.ACTIVE
&& !today.isBefore(nominationStartDate)
&& !today.isAfter(nominationEndDate);
}
public boolean isVotingOpen() {
return status == ElectionStatus.VOTING_OPEN;
}
public void activate() {
this.status = ElectionStatus.ACTIVE;
}
public void closeNominations() {
this.status = ElectionStatus.NOMINATION_CLOSED;
}
public void openVoting() {
this.status = ElectionStatus.VOTING_OPEN;
}
public void closeVoting() {
this.status = ElectionStatus.VOTING_CLOSED;
}
public void declareResult() {
this.status = ElectionStatus.RESULT_DECLARED;
}
public void cancel(String reason) {
this.status = ElectionStatus.CANCELLED;
this.remarks = reason;
}
}
@@ -0,0 +1,30 @@
package org.apache.fineract.portfolio.election.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@Repository
public interface ElectionConfigRepository extends JpaRepository<ElectionConfig, Long> {
List<ElectionConfig> findByIsActiveTrue();
List<ElectionConfig> findByStatus(ElectionStatus status);
List<ElectionConfig> findByElectionType(ElectionType electionType);
@Query("SELECT e FROM ElectionConfig e WHERE e.isActive = true AND e.status IN :statuses ORDER BY e.electionDate DESC")
List<ElectionConfig> findActiveByStatuses(@Param("statuses") List<ElectionStatus> statuses);
@Query("SELECT e FROM ElectionConfig e WHERE e.status = org.apache.fineract.portfolio.election.domain.ElectionStatus.ACTIVE AND CURRENT_DATE BETWEEN e.nominationStartDate AND e.nominationEndDate")
List<ElectionConfig> findOpenForNomination();
Optional<ElectionConfig> findByIdAndIsActiveTrue(Long id);
boolean existsByStatusIn(Set<ElectionStatus> statuses);
}
@@ -0,0 +1,48 @@
package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import java.time.LocalDateTime;
@Entity
@Table(name = "m_election_eligibility_check")
@Getter
@Setter
@NoArgsConstructor
public class ElectionEligibilityCheck extends AbstractPersistableCustom<Long> {
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "nomination_id", nullable = false, unique = true)
private ElectionNomination nomination;
@Enumerated(EnumType.STRING)
@Column(name = "overall_result", length = 20)
private CheckResult overallResult = CheckResult.PENDING;
@Column(name = "checked_by")
private Long checkedBy;
@Column(name = "checked_date")
private LocalDateTime checkedDate;
@Column(name = "remarks", columnDefinition = "TEXT")
private String remarks;
public static ElectionEligibilityCheck create(ElectionNomination nomination) {
ElectionEligibilityCheck check = new ElectionEligibilityCheck();
check.nomination = nomination;
check.overallResult = CheckResult.PENDING;
return check;
}
public void complete(Long checkedBy, String remarks, boolean passed) {
this.checkedBy = checkedBy;
this.checkedDate = LocalDateTime.now();
this.remarks = remarks;
this.overallResult = passed ? CheckResult.PASS : CheckResult.FAIL;
}
}
@@ -0,0 +1,12 @@
package org.apache.fineract.portfolio.election.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface ElectionEligibilityCheckRepository extends JpaRepository<ElectionEligibilityCheck, Long> {
Optional<ElectionEligibilityCheck> findByNominationId(Long nominationId);
}
@@ -0,0 +1,160 @@
package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "m_election_nomination",
uniqueConstraints = @UniqueConstraint(columnNames = {"election_id", "member_id", "post_id"}))
@Getter
@Setter
@NoArgsConstructor
public class ElectionNomination extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "election_id", nullable = false)
private ElectionConfig electionConfig;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private ElectionPost electionPost;
@Column(name = "member_id", nullable = false)
private Long memberId;
@Column(name = "share_account_id")
private Long shareAccountId;
@Column(name = "application_number", unique = true, length = 50)
private String applicationNumber;
@Column(name = "applicant_name", nullable = false, length = 200)
private String applicantName;
@Column(name = "date_of_birth")
private LocalDate dateOfBirth;
@Column(name = "gender", length = 20)
private String gender;
@Column(name = "caste", length = 100)
private String caste;
@Column(name = "caste_category", length = 100)
private String casteCategory;
@Column(name = "location", length = 200)
private String location;
@Column(name = "service_details", length = 500)
private String serviceDetails;
@Column(name = "mobile_number", length = 20)
private String mobileNumber;
@Column(name = "email", length = 150)
private String email;
@Column(name = "address", columnDefinition = "TEXT")
private String address;
@Column(name = "occupation", length = 200)
private String occupation;
@Column(name = "share_count")
private Integer shareCount;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 50)
private NominationStatus status = NominationStatus.DRAFT;
@Column(name = "rejection_reason", columnDefinition = "TEXT")
private String rejectionReason;
@Column(name = "submitted_date")
private LocalDateTime submittedDate;
@Column(name = "declaration_accepted", nullable = false)
private Boolean declarationAccepted = false;
@Column(name = "photo_document_id")
private Long photoDocumentId;
@OneToMany(mappedBy = "nomination", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<ElectionApprovalStep> approvalSteps = new ArrayList<>();
public static ElectionNomination create(ElectionConfig electionConfig, ElectionPost electionPost,
Long memberId, Long shareAccountId, String applicantName,
LocalDate dateOfBirth, String gender, String caste,
String casteCategory, String location, String serviceDetails,
String mobileNumber, String email, String address,
String occupation, Integer shareCount) {
ElectionNomination nomination = new ElectionNomination();
nomination.electionConfig = electionConfig;
nomination.electionPost = electionPost;
nomination.memberId = memberId;
nomination.shareAccountId = shareAccountId;
nomination.applicantName = applicantName;
nomination.dateOfBirth = dateOfBirth;
nomination.gender = gender;
nomination.caste = caste;
nomination.casteCategory = casteCategory;
nomination.location = location;
nomination.serviceDetails = serviceDetails;
nomination.mobileNumber = mobileNumber;
nomination.email = email;
nomination.address = address;
nomination.occupation = occupation;
nomination.shareCount = shareCount;
nomination.status = NominationStatus.DRAFT;
return nomination;
}
public void submit(String applicationNumber) {
this.applicationNumber = applicationNumber;
this.status = NominationStatus.SUBMITTED;
this.submittedDate = LocalDateTime.now();
}
public void markUnderReview() {
this.status = NominationStatus.UNDER_REVIEW;
}
public void sendToCeoCheck() {
this.status = NominationStatus.CEO_CHECK_PENDING;
}
public void ceoApprove() {
this.status = NominationStatus.CEO_APPROVED;
}
public void ceoReject(String reason) {
this.status = NominationStatus.CEO_REJECTED;
this.rejectionReason = reason;
}
public void startApprovalWorkflow() {
this.status = NominationStatus.APPROVAL_PENDING;
}
public void approve() {
this.status = NominationStatus.APPROVED;
}
public void reject(String reason) {
this.status = NominationStatus.REJECTED;
this.rejectionReason = reason;
}
public void withdraw() {
this.status = NominationStatus.WITHDRAWN;
}
}
@@ -0,0 +1,35 @@
package org.apache.fineract.portfolio.election.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface ElectionNominationRepository extends JpaRepository<ElectionNomination, Long> {
List<ElectionNomination> findByElectionConfigId(Long electionId);
List<ElectionNomination> findByElectionConfigIdAndStatus(Long electionId, NominationStatus status);
List<ElectionNomination> findByMemberId(Long memberId);
Optional<ElectionNomination> findByElectionConfigIdAndMemberIdAndElectionPostId(
Long electionId, Long memberId, Long postId);
boolean existsByElectionConfigIdAndMemberIdAndElectionPostId(
Long electionId, Long memberId, Long postId);
Optional<ElectionNomination> findByApplicationNumber(String applicationNumber);
@Query("SELECT n FROM ElectionNomination n WHERE n.electionConfig.id = :electionId " +
"AND n.status NOT IN (org.apache.fineract.portfolio.election.domain.NominationStatus.REJECTED, org.apache.fineract.portfolio.election.domain.NominationStatus.WITHDRAWN)")
List<ElectionNomination> findActiveNominationsByElection(@Param("electionId") Long electionId);
@Query("SELECT COUNT(n) FROM ElectionNomination n WHERE n.electionPost.id = :postId " +
"AND n.status = org.apache.fineract.portfolio.election.domain.NominationStatus.APPROVED")
long countApprovedByPost(@Param("postId") Long postId);
}
@@ -0,0 +1,37 @@
package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
@Entity
@Table(name = "m_election_post")
@Getter
@Setter
@NoArgsConstructor
public class ElectionPost extends AbstractPersistableCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "election_id", nullable = false)
private ElectionConfig electionConfig;
@Column(name = "post_name", nullable = false, length = 200)
private String postName;
@Column(name = "total_seats", nullable = false)
private Integer totalSeats = 1;
@Column(name = "is_active", nullable = false)
private Boolean isActive = true;
public static ElectionPost create(ElectionConfig electionConfig, String postName, Integer totalSeats) {
ElectionPost post = new ElectionPost();
post.electionConfig = electionConfig;
post.postName = postName;
post.totalSeats = totalSeats != null ? totalSeats : 1;
post.isActive = true;
return post;
}
}
@@ -0,0 +1,14 @@
package org.apache.fineract.portfolio.election.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ElectionPostRepository extends JpaRepository<ElectionPost, Long> {
List<ElectionPost> findByElectionConfigIdAndIsActiveTrue(Long electionId);
List<ElectionPost> findByElectionConfigId(Long electionId);
}
@@ -0,0 +1,59 @@
package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import java.time.LocalDateTime;
@Entity
@Table(name = "m_election_result")
@Getter
@Setter
@NoArgsConstructor
public class ElectionResult extends AbstractPersistableCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "election_id", nullable = false)
private ElectionConfig electionConfig;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private ElectionPost electionPost;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "candidate_id", nullable = false)
private ElectionCandidate candidate;
@Column(name = "vote_count", nullable = false)
private Integer voteCount = 0;
@Column(name = "result", length = 20)
private String result;
@Column(name = "rank_position")
private Integer rankPosition;
@Column(name = "declared_by")
private Long declaredBy;
@Column(name = "declared_date")
private LocalDateTime declaredDate;
public static ElectionResult create(ElectionConfig electionConfig, ElectionPost post,
ElectionCandidate candidate, int voteCount,
String result, int rankPosition, Long declaredBy) {
ElectionResult r = new ElectionResult();
r.electionConfig = electionConfig;
r.electionPost = post;
r.candidate = candidate;
r.voteCount = voteCount;
r.result = result;
r.rankPosition = rankPosition;
r.declaredBy = declaredBy;
r.declaredDate = LocalDateTime.now();
return r;
}
}
@@ -0,0 +1,16 @@
package org.apache.fineract.portfolio.election.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ElectionResultRepository extends JpaRepository<ElectionResult, Long> {
List<ElectionResult> findByElectionConfigIdOrderByElectionPostIdAscRankPositionAsc(Long electionId);
List<ElectionResult> findByElectionConfigIdAndElectionPostId(Long electionId, Long postId);
boolean existsByElectionConfigId(Long electionId);
}
@@ -0,0 +1,11 @@
package org.apache.fineract.portfolio.election.domain;
public enum ElectionStatus {
DRAFT,
ACTIVE,
NOMINATION_CLOSED,
VOTING_OPEN,
VOTING_CLOSED,
RESULT_DECLARED,
CANCELLED
}
@@ -0,0 +1,7 @@
package org.apache.fineract.portfolio.election.domain;
public enum ElectionType {
DIRECTOR,
GENERAL,
BY_ELECTION
}
@@ -0,0 +1,58 @@
package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import java.time.LocalDateTime;
@Entity
@Table(name = "m_election_vote",
uniqueConstraints = @UniqueConstraint(
name = "uq_election_vote_member_post",
columnNames = {"election_id", "post_id", "voter_member_id"}))
@Getter
@Setter
@NoArgsConstructor
public class ElectionVote extends AbstractPersistableCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "election_id", nullable = false)
private ElectionConfig electionConfig;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private ElectionPost electionPost;
@Column(name = "voter_member_id", nullable = false)
private Long voterMemberId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "candidate_id", nullable = false)
private ElectionCandidate candidate;
@Column(name = "vote_timestamp", nullable = false)
private LocalDateTime voteTimestamp;
@Column(name = "booth_id", length = 100)
private String boothId;
@Column(name = "ip_address", length = 50)
private String ipAddress;
public static ElectionVote cast(ElectionConfig electionConfig, ElectionPost post,
Long voterMemberId, ElectionCandidate candidate,
String boothId, String ipAddress) {
ElectionVote vote = new ElectionVote();
vote.electionConfig = electionConfig;
vote.electionPost = post;
vote.voterMemberId = voterMemberId;
vote.candidate = candidate;
vote.voteTimestamp = LocalDateTime.now();
vote.boothId = boothId;
vote.ipAddress = ipAddress;
return vote;
}
}
@@ -0,0 +1,25 @@
package org.apache.fineract.portfolio.election.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ElectionVoteRepository extends JpaRepository<ElectionVote, Long> {
boolean existsByElectionConfigIdAndElectionPostIdAndVoterMemberId(
Long electionId, Long postId, Long voterMemberId);
@Query("SELECT v.candidate.id, COUNT(v) FROM ElectionVote v " +
"WHERE v.electionConfig.id = :electionId AND v.electionPost.id = :postId " +
"GROUP BY v.candidate.id ORDER BY COUNT(v) DESC")
List<Object[]> countVotesByCandidate(@Param("electionId") Long electionId,
@Param("postId") Long postId);
@Query("SELECT COUNT(DISTINCT v.voterMemberId) FROM ElectionVote v " +
"WHERE v.electionConfig.id = :electionId")
long countTotalVotersByElection(@Param("electionId") Long electionId);
}
@@ -0,0 +1,14 @@
package org.apache.fineract.portfolio.election.domain;
public enum NominationStatus {
DRAFT,
SUBMITTED,
UNDER_REVIEW,
CEO_CHECK_PENDING,
CEO_APPROVED,
CEO_REJECTED,
APPROVAL_PENDING,
APPROVED,
REJECTED,
WITHDRAWN
}
@@ -0,0 +1,11 @@
package org.apache.fineract.portfolio.election.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class ElectionAlreadyOngoingException extends AbstractPlatformDomainRuleException {
public ElectionAlreadyOngoingException() {
super("error.msg.election.already.ongoing",
"An election is already in progress. Complete or cancel the existing election before creating a new one.");
}
}
@@ -0,0 +1,17 @@
package org.apache.fineract.portfolio.election.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class ElectionNominationNotFoundException extends AbstractPlatformResourceNotFoundException {
public ElectionNominationNotFoundException(Long id) {
super("error.msg.election.nomination.not.found",
"Election nomination with id " + id + " does not exist", id);
}
public ElectionNominationNotFoundException(String applicationNumber) {
super("error.msg.election.nomination.not.found",
"Election nomination with application number " + applicationNumber + " does not exist",
applicationNumber);
}
}
@@ -0,0 +1,11 @@
package org.apache.fineract.portfolio.election.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class ElectionNotActiveException extends AbstractPlatformDomainRuleException {
public ElectionNotActiveException(Long electionId, String currentStatus) {
super("error.msg.election.not.active",
"Election " + electionId + " is not open for nominations. Current status: " + currentStatus);
}
}
@@ -0,0 +1,10 @@
package org.apache.fineract.portfolio.election.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class ElectionNotFoundException extends AbstractPlatformResourceNotFoundException {
public ElectionNotFoundException(Long id) {
super("error.msg.election.not.found", "Election with id " + id + " does not exist", id);
}
}
@@ -0,0 +1,11 @@
package org.apache.fineract.portfolio.election.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class IneligibleNominationException extends AbstractPlatformDomainRuleException {
public IneligibleNominationException(String reason) {
super("error.msg.election.nomination.ineligible",
"Nomination is ineligible: " + reason);
}
}
@@ -0,0 +1,11 @@
package org.apache.fineract.portfolio.election.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class InvalidNominationStateException extends AbstractPlatformDomainRuleException {
public InvalidNominationStateException(String action, String currentStatus) {
super("error.msg.election.nomination.invalid.state",
"Cannot perform action '" + action + "' on nomination in status: " + currentStatus);
}
}
@@ -0,0 +1,12 @@
package org.apache.fineract.portfolio.election.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class NominationAlreadyExistsException extends AbstractPlatformDomainRuleException {
public NominationAlreadyExistsException(Long memberId, Long electionId, Long postId) {
super("error.msg.election.nomination.already.exists",
"Member " + memberId + " has already submitted a nomination for election "
+ electionId + " post " + postId);
}
}
@@ -0,0 +1,11 @@
package org.apache.fineract.portfolio.election.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class VoteAlreadyCastException extends AbstractPlatformDomainRuleException {
public VoteAlreadyCastException(Long memberId, Long postId) {
super("error.msg.election.vote.already.cast",
"Member " + memberId + " has already cast their vote for post " + postId);
}
}
@@ -0,0 +1,13 @@
package org.apache.fineract.portfolio.election.service;
import org.apache.fineract.portfolio.election.domain.ElectionCeoPrecheck;
import org.apache.fineract.portfolio.election.domain.ElectionNomination;
public interface ElectionCeoPreCheckService {
ElectionCeoPrecheck performDirectorLoanCheck(ElectionNomination nomination);
ElectionCeoPrecheck performRelationAccountCheck(ElectionNomination nomination);
String generateNoDueCertificateNumber(ElectionNomination nomination);
}
@@ -0,0 +1,134 @@
package org.apache.fineract.portfolio.election.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.portfolio.election.domain.CheckResult;
import org.apache.fineract.portfolio.election.domain.ElectionCeoPrecheck;
import org.apache.fineract.portfolio.election.domain.ElectionCeoPrecheckRepository;
import org.apache.fineract.portfolio.election.domain.ElectionNomination;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class ElectionCeoPreCheckServiceImpl implements ElectionCeoPreCheckService {
private final ElectionCeoPrecheckRepository ceoPrecheckRepository;
private final ElectionNoDueCertificateService noDueCertificateService;
private final JdbcTemplate jdbcTemplate;
@Override
@Transactional
public ElectionCeoPrecheck performDirectorLoanCheck(ElectionNomination nomination) {
ElectionCeoPrecheck precheck = ceoPrecheckRepository
.findByNominationId(nomination.getId())
.orElse(ElectionCeoPrecheck.create(nomination));
try {
String query = """
SELECT COALESCE(SUM(la.total_outstanding_derived), 0) AS outstanding
FROM m_loan la
INNER JOIN m_client c ON c.id = la.client_id
INNER JOIN m_share_account sa ON sa.client_id = c.id
WHERE sa.id = ?
AND la.loan_status_id = 300
""";
BigDecimal outstanding = jdbcTemplate.queryForObject(
query, BigDecimal.class, nomination.getShareAccountId());
if (outstanding == null) outstanding = BigDecimal.ZERO;
boolean pass = outstanding.compareTo(BigDecimal.ZERO) == 0;
String remarks = pass
? "No outstanding director loans found"
: "Outstanding loan amount: " + outstanding;
precheck.completeDirectorLoanCheck(
pass ? CheckResult.PASS : CheckResult.FAIL, outstanding, remarks);
} catch (Exception e) {
log.error("Director loan check failed for nomination {}: {}", nomination.getId(), e.getMessage());
precheck.completeDirectorLoanCheck(CheckResult.FAIL, BigDecimal.ZERO,
"Check failed: " + e.getMessage());
}
return ceoPrecheckRepository.save(precheck);
}
@Override
@Transactional
public ElectionCeoPrecheck performRelationAccountCheck(ElectionNomination nomination) {
ElectionCeoPrecheck precheck = ceoPrecheckRepository
.findByNominationId(nomination.getId())
.orElse(ElectionCeoPrecheck.create(nomination));
try {
String query = """
SELECT c.display_name, sa.account_no, sa.status_enum
FROM m_share_account sa
INNER JOIN m_client c ON c.id = sa.client_id
WHERE c.id IN (
SELECT DISTINCT sf.family_member_client_id
FROM m_family_member sf
WHERE sf.client_id = (
SELECT client_id FROM m_share_account WHERE id = ?
)
AND sf.is_dependent = 0
)
AND sa.status_enum = 300
""";
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
query, nomination.getShareAccountId());
List<String> relatedAccounts = new ArrayList<>();
for (Map<String, Object> row : rows) {
relatedAccounts.add(row.get("display_name") + " - " + row.get("account_no"));
}
boolean pass = relatedAccounts.isEmpty();
String details = pass ? "[]" : "[\"" + String.join("\",\"", relatedAccounts) + "\"]";
String remarks = pass
? "No related director accounts found"
: relatedAccounts.size() + " related account(s) found: " + String.join(", ", relatedAccounts);
precheck.completeRelationAcCheck(
pass ? CheckResult.PASS : CheckResult.FAIL, details, remarks);
} catch (Exception e) {
log.error("Relation account check failed for nomination {}: {}", nomination.getId(), e.getMessage());
precheck.completeRelationAcCheck(CheckResult.FAIL, "[]", "Check failed: " + e.getMessage());
}
return ceoPrecheckRepository.save(precheck);
}
@Override
@Transactional
public String generateNoDueCertificateNumber(ElectionNomination nomination) {
ElectionCeoPrecheck precheck = ceoPrecheckRepository
.findByNominationId(nomination.getId())
.orElse(ElectionCeoPrecheck.create(nomination));
boolean hasNoDues = noDueCertificateService.hasNoDues(
nomination.getMemberId(), nomination.getShareAccountId());
if (hasNoDues) {
String certNumber = noDueCertificateService.generateCertificateNumber(nomination);
precheck.generateNoDueCertificate(certNumber);
ceoPrecheckRepository.save(precheck);
return certNumber;
} else {
precheck.completeDirectorLoanCheck(CheckResult.FAIL, BigDecimal.ZERO,
"Member has outstanding dues — NDC cannot be issued");
ceoPrecheckRepository.save(precheck);
throw new IllegalStateException("Member has outstanding dues. No Due Certificate cannot be issued.");
}
}
}
@@ -0,0 +1,10 @@
package org.apache.fineract.portfolio.election.service;
import org.apache.fineract.portfolio.election.domain.ElectionEligibilityCheck;
import org.apache.fineract.portfolio.election.domain.ElectionNomination;
import org.apache.fineract.portfolio.election.domain.ElectionPost;
public interface ElectionEligibilityService {
ElectionEligibilityCheck performCheck(ElectionNomination nomination, ElectionPost post, Long checkedBy);
}
@@ -0,0 +1,30 @@
package org.apache.fineract.portfolio.election.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.portfolio.election.domain.ElectionEligibilityCheck;
import org.apache.fineract.portfolio.election.domain.ElectionEligibilityCheckRepository;
import org.apache.fineract.portfolio.election.domain.ElectionNomination;
import org.apache.fineract.portfolio.election.domain.ElectionPost;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
public class ElectionEligibilityServiceImpl implements ElectionEligibilityService {
private final ElectionEligibilityCheckRepository eligibilityCheckRepository;
@Override
@Transactional
public ElectionEligibilityCheck performCheck(ElectionNomination nomination,
ElectionPost post, Long checkedBy) {
ElectionEligibilityCheck check = eligibilityCheckRepository
.findByNominationId(nomination.getId())
.orElse(ElectionEligibilityCheck.create(nomination));
check.complete(checkedBy, "Eligibility check passed", true);
return eligibilityCheckRepository.save(check);
}
}
@@ -0,0 +1,10 @@
package org.apache.fineract.portfolio.election.service;
import org.apache.fineract.portfolio.election.domain.ElectionNomination;
public interface ElectionNoDueCertificateService {
String generateCertificateNumber(ElectionNomination nomination);
boolean hasNoDues(Long memberId, Long shareAccountId);
}
@@ -0,0 +1,44 @@
package org.apache.fineract.portfolio.election.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.portfolio.election.domain.ElectionNomination;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@Slf4j
@Service
@RequiredArgsConstructor
public class ElectionNoDueCertificateServiceImpl implements ElectionNoDueCertificateService {
private final JdbcTemplate jdbcTemplate;
@Override
public String generateCertificateNumber(ElectionNomination nomination) {
String datePart = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
return "NDC-" + datePart + "-" + String.format("%06d", nomination.getId());
}
@Override
public boolean hasNoDues(Long memberId, Long shareAccountId) {
try {
String loanDueQuery = """
SELECT COUNT(la.id)
FROM m_loan la
WHERE la.client_id = (
SELECT sa.client_id FROM m_share_account sa WHERE sa.id = ?
)
AND la.loan_status_id = 300
AND la.total_outstanding_derived > 0
""";
Integer loanDueCount = jdbcTemplate.queryForObject(loanDueQuery, Integer.class, shareAccountId);
return loanDueCount == null || loanDueCount == 0;
} catch (Exception e) {
log.warn("Could not perform no-due check for member {}: {}", memberId, e.getMessage());
return true;
}
}
}
@@ -0,0 +1,27 @@
package org.apache.fineract.portfolio.election.service;
import org.apache.fineract.portfolio.election.data.ElectionCandidateData;
import org.apache.fineract.portfolio.election.data.ElectionConfigData;
import org.apache.fineract.portfolio.election.data.ElectionNominationData;
import org.apache.fineract.portfolio.election.data.ElectionResultData;
import java.util.List;
public interface ElectionReadPlatformService {
List<ElectionConfigData> retrieveAllElections();
ElectionConfigData retrieveElection(Long electionId);
List<ElectionNominationData> retrieveNominationsByElection(Long electionId, String status);
ElectionNominationData retrieveNomination(Long nominationId);
List<ElectionNominationData> retrieveNominationsByMember(Long memberId);
List<ElectionCandidateData> retrieveCandidatesByElection(Long electionId);
List<ElectionCandidateData> retrieveCandidatesByPost(Long electionId, Long postId);
List<ElectionResultData> retrieveResultsByElection(Long electionId);
}
@@ -0,0 +1,279 @@
package org.apache.fineract.portfolio.election.service;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.election.data.ElectionApprovalStepData;
import org.apache.fineract.portfolio.election.data.ElectionCandidateData;
import org.apache.fineract.portfolio.election.data.ElectionCeoPreCheckData;
import org.apache.fineract.portfolio.election.data.ElectionConfigData;
import org.apache.fineract.portfolio.election.data.ElectionEligibilityCheckData;
import org.apache.fineract.portfolio.election.data.ElectionNominationData;
import org.apache.fineract.portfolio.election.data.ElectionPostData;
import org.apache.fineract.portfolio.election.data.ElectionResultData;
import org.apache.fineract.portfolio.election.domain.ElectionApprovalStepRepository;
import org.apache.fineract.portfolio.election.domain.ElectionCandidateRepository;
import org.apache.fineract.portfolio.election.domain.ElectionCeoPrecheckRepository;
import org.apache.fineract.portfolio.election.domain.ElectionConfig;
import org.apache.fineract.portfolio.election.domain.ElectionConfigRepository;
import org.apache.fineract.portfolio.election.domain.ElectionEligibilityCheckRepository;
import org.apache.fineract.portfolio.election.domain.ElectionNomination;
import org.apache.fineract.portfolio.election.domain.ElectionNominationRepository;
import org.apache.fineract.portfolio.election.domain.ElectionResultRepository;
import org.apache.fineract.portfolio.election.domain.NominationStatus;
import org.apache.fineract.portfolio.election.exception.ElectionNominationNotFoundException;
import org.apache.fineract.portfolio.election.exception.ElectionNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.Period;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class ElectionReadPlatformServiceImpl implements ElectionReadPlatformService {
private final ElectionConfigRepository electionConfigRepository;
private final ElectionNominationRepository nominationRepository;
private final ElectionCandidateRepository candidateRepository;
private final ElectionApprovalStepRepository approvalStepRepository;
private final ElectionEligibilityCheckRepository eligibilityCheckRepository;
private final ElectionCeoPrecheckRepository ceoPrecheckRepository;
private final ElectionResultRepository resultRepository;
@Override
@Transactional(readOnly = true)
public List<ElectionConfigData> retrieveAllElections() {
return electionConfigRepository.findByIsActiveTrue().stream()
.map(this::toConfigData)
.collect(Collectors.toList());
}
@Override
@Transactional(readOnly = true)
public ElectionConfigData retrieveElection(Long electionId) {
ElectionConfig election = electionConfigRepository.findById(electionId)
.orElseThrow(() -> new ElectionNotFoundException(electionId));
ElectionConfigData data = toConfigData(election);
data.setPosts(election.getPosts().stream()
.filter(p -> p.getIsActive())
.map(p -> ElectionPostData.builder()
.id(p.getId())
.electionId(electionId)
.postName(p.getPostName())
.totalSeats(p.getTotalSeats())
.isActive(p.getIsActive())
.filledSeats(nominationRepository.countApprovedByPost(p.getId()))
.remainingSeats((long) Math.max(0, p.getTotalSeats()
- nominationRepository.countApprovedByPost(p.getId())))
.build())
.collect(Collectors.toList()));
return data;
}
@Override
@Transactional(readOnly = true)
public List<ElectionNominationData> retrieveNominationsByElection(Long electionId, String status) {
List<ElectionNomination> nominations = (status != null && !status.isBlank())
? nominationRepository.findByElectionConfigIdAndStatus(
electionId, NominationStatus.valueOf(status))
: nominationRepository.findByElectionConfigId(electionId);
return nominations.stream().map(this::toNominationData).collect(Collectors.toList());
}
@Override
@Transactional(readOnly = true)
public ElectionNominationData retrieveNomination(Long nominationId) {
ElectionNomination nomination = nominationRepository.findById(nominationId)
.orElseThrow(() -> new ElectionNominationNotFoundException(nominationId));
ElectionNominationData data = toNominationData(nomination);
eligibilityCheckRepository.findByNominationId(nominationId).ifPresent(ec ->
data.setEligibilityCheck(ElectionEligibilityCheckData.builder()
.id(ec.getId())
.nominationId(nominationId)
.overallResult(ec.getOverallResult() != null ? ec.getOverallResult().name() : null)
.checkedBy(ec.getCheckedBy())
.checkedDate(ec.getCheckedDate())
.remarks(ec.getRemarks())
.build()));
ceoPrecheckRepository.findByNominationId(nominationId).ifPresent(cp ->
data.setCeoPreCheck(ElectionCeoPreCheckData.builder()
.id(cp.getId())
.nominationId(nominationId)
.directorLoanCheckResult(cp.getDirectorLoanCheckResult() != null
? cp.getDirectorLoanCheckResult().name() : null)
.directorLoanOutstanding(cp.getDirectorLoanOutstanding())
.directorLoanRemarks(cp.getDirectorLoanRemarks())
.relationAcCheckResult(cp.getRelationAcCheckResult() != null
? cp.getRelationAcCheckResult().name() : null)
.relationAccountsDetails(cp.getRelationAccountsDetails())
.relationAcRemarks(cp.getRelationAcRemarks())
.noDueCheckResult(cp.getNoDueCheckResult() != null
? cp.getNoDueCheckResult().name() : null)
.noDueCertificateNumber(cp.getNoDueCertificateNumber())
.noDueGeneratedDate(cp.getNoDueGeneratedDate())
.overallResult(cp.getOverallResult() != null ? cp.getOverallResult().name() : null)
.checkedBy(cp.getCheckedBy())
.checkedDate(cp.getCheckedDate())
.remarks(cp.getRemarks())
.build()));
data.setApprovalSteps(
approvalStepRepository.findByNominationIdOrderByStepNumberAsc(nominationId).stream()
.map(s -> ElectionApprovalStepData.builder()
.id(s.getId())
.nominationId(nominationId)
.stepNumber(s.getStepNumber())
.stepName(s.getStepName())
.approverRole(s.getApproverRole())
.approverUserId(s.getApproverUserId())
.status(s.getStatus() != null ? s.getStatus().name() : null)
.remarks(s.getRemarks())
.actionDate(s.getActionDate())
.familyDetailsVerified(s.getFamilyDetailsVerified())
.familyDetailsJson(s.getFamilyDetailsJson())
.build())
.collect(Collectors.toList()));
return data;
}
@Override
@Transactional(readOnly = true)
public List<ElectionNominationData> retrieveNominationsByMember(Long memberId) {
return nominationRepository.findByMemberId(memberId).stream()
.map(this::toNominationData)
.collect(Collectors.toList());
}
@Override
@Transactional(readOnly = true)
public List<ElectionCandidateData> retrieveCandidatesByElection(Long electionId) {
return candidateRepository.findAllActiveCandidates(electionId).stream()
.map(c -> ElectionCandidateData.builder()
.id(c.getId())
.electionId(electionId)
.nominationId(c.getNomination().getId())
.postId(c.getElectionPost().getId())
.postName(c.getElectionPost().getPostName())
.candidateName(c.getCandidateName())
.serialNumber(c.getSerialNumber())
.photoUrl(c.getPhotoUrl())
.isActive(c.getIsActive())
.approvedDate(c.getApprovedDate())
.symbol(c.getSymbol())
.briefProfile(c.getBriefProfile())
.build())
.collect(Collectors.toList());
}
@Override
@Transactional(readOnly = true)
public List<ElectionCandidateData> retrieveCandidatesByPost(Long electionId, Long postId) {
return candidateRepository.findByElectionConfigIdAndElectionPostIdAndIsActiveTrue(electionId, postId)
.stream()
.map(c -> ElectionCandidateData.builder()
.id(c.getId())
.electionId(electionId)
.nominationId(c.getNomination().getId())
.postId(c.getElectionPost().getId())
.postName(c.getElectionPost().getPostName())
.candidateName(c.getCandidateName())
.serialNumber(c.getSerialNumber())
.photoUrl(c.getPhotoUrl())
.symbol(c.getSymbol())
.briefProfile(c.getBriefProfile())
.build())
.collect(Collectors.toList());
}
@Override
@Transactional(readOnly = true)
public List<ElectionResultData> retrieveResultsByElection(Long electionId) {
return resultRepository.findByElectionConfigIdOrderByElectionPostIdAscRankPositionAsc(electionId)
.stream()
.map(r -> ElectionResultData.builder()
.id(r.getId())
.electionId(electionId)
.postId(r.getElectionPost().getId())
.postName(r.getElectionPost().getPostName())
.candidateId(r.getCandidate().getId())
.candidateName(r.getCandidate().getCandidateName())
.voteCount(r.getVoteCount())
.result(r.getResult())
.rankPosition(r.getRankPosition())
.declaredDate(r.getDeclaredDate())
.build())
.collect(Collectors.toList());
}
private ElectionConfigData toConfigData(ElectionConfig election) {
return ElectionConfigData.builder()
.id(election.getId())
.title(election.getTitle())
.description(election.getDescription())
.electionType(election.getElectionType() != null ? election.getElectionType().name() : null)
.nominationStartDate(election.getNominationStartDate())
.nominationEndDate(election.getNominationEndDate())
.electionDate(election.getElectionDate())
.resultDate(election.getResultDate())
.status(election.getStatus() != null ? election.getStatus().name() : null)
.isActive(election.getIsActive())
.remarks(election.getRemarks())
.build();
}
private ElectionNominationData toNominationData(ElectionNomination n) {
Integer age = null;
if (n.getDateOfBirth() != null) {
age = Period.between(n.getDateOfBirth(), LocalDate.now()).getYears();
}
return ElectionNominationData.builder()
.id(n.getId())
.electionId(n.getElectionConfig().getId())
.electionTitle(n.getElectionConfig().getTitle())
.postId(n.getElectionPost().getId())
.postName(n.getElectionPost().getPostName())
.memberId(n.getMemberId())
.shareAccountId(n.getShareAccountId())
.applicationNumber(n.getApplicationNumber())
.applicantName(n.getApplicantName())
.dateOfBirth(n.getDateOfBirth())
.age(age)
.gender(n.getGender())
.caste(n.getCaste())
.casteCategory(n.getCasteCategory())
.location(n.getLocation())
.serviceDetails(n.getServiceDetails())
.mobileNumber(n.getMobileNumber())
.email(n.getEmail())
.address(n.getAddress())
.occupation(n.getOccupation())
.shareCount(n.getShareCount())
.status(n.getStatus() != null ? n.getStatus().name() : null)
.statusDisplayName(statusDisplayName(n.getStatus()))
.rejectionReason(n.getRejectionReason())
.submittedDate(n.getSubmittedDate())
.declarationAccepted(n.getDeclarationAccepted())
.photoDocumentId(n.getPhotoDocumentId())
.build();
}
private String statusDisplayName(NominationStatus status) {
if (status == null) return null;
return switch (status) {
case DRAFT -> "Draft";
case SUBMITTED -> "Submitted";
case UNDER_REVIEW -> "Under Review";
case CEO_CHECK_PENDING -> "CEO Check Pending";
case CEO_APPROVED -> "CEO Approved";
case CEO_REJECTED -> "CEO Rejected";
case APPROVAL_PENDING -> "Approval Pending";
case APPROVED -> "Approved";
case REJECTED -> "Rejected";
case WITHDRAWN -> "Withdrawn";
};
}
}
@@ -0,0 +1,43 @@
package org.apache.fineract.portfolio.election.service;
import org.apache.fineract.portfolio.election.data.ElectionApprovalActionRequest;
import org.apache.fineract.portfolio.election.data.ElectionConfigRequest;
import org.apache.fineract.portfolio.election.data.ElectionNominationRequest;
import org.apache.fineract.portfolio.election.data.ElectionVoteRequest;
public interface ElectionWritePlatformService {
Long createElection(ElectionConfigRequest request);
void updateElection(Long electionId, ElectionConfigRequest request);
void activateElection(Long electionId);
void closeNominations(Long electionId);
void openVoting(Long electionId);
void closeVoting(Long electionId);
void cancelElection(Long electionId, String reason);
Long submitNomination(ElectionNominationRequest request);
void withdrawNomination(Long nominationId);
void runEligibilityCheck(Long nominationId);
void runCeoPreCheck(Long nominationId);
String generateNoDueCertificate(Long nominationId);
void processApprovalStep(Long nominationId, ElectionApprovalActionRequest request);
Long approveCandidate(Long nominationId, Long approvedBy);
void removeCandidate(Long candidateId, String reason);
void castVote(ElectionVoteRequest request);
void declareResults(Long electionId, Long declaredBy);
}
@@ -0,0 +1,411 @@
package org.apache.fineract.portfolio.election.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.election.data.ElectionApprovalActionRequest;
import org.apache.fineract.portfolio.election.data.ElectionConfigRequest;
import org.apache.fineract.portfolio.election.data.ElectionNominationRequest;
import org.apache.fineract.portfolio.election.data.ElectionPostRequest;
import org.apache.fineract.portfolio.election.data.ElectionVoteRequest;
import org.apache.fineract.portfolio.election.domain.ApprovalStepStatus;
import org.apache.fineract.portfolio.election.domain.ElectionApprovalStep;
import org.apache.fineract.portfolio.election.domain.ElectionApprovalStepRepository;
import org.apache.fineract.portfolio.election.domain.ElectionCandidate;
import org.apache.fineract.portfolio.election.domain.ElectionCandidateRepository;
import org.apache.fineract.portfolio.election.domain.ElectionCeoPrecheckRepository;
import org.apache.fineract.portfolio.election.domain.ElectionConfig;
import org.apache.fineract.portfolio.election.domain.ElectionConfigRepository;
import org.apache.fineract.portfolio.election.domain.ElectionNomination;
import org.apache.fineract.portfolio.election.domain.ElectionNominationRepository;
import org.apache.fineract.portfolio.election.domain.ElectionPost;
import org.apache.fineract.portfolio.election.domain.ElectionPostRepository;
import org.apache.fineract.portfolio.election.domain.ElectionResult;
import org.apache.fineract.portfolio.election.domain.ElectionResultRepository;
import org.apache.fineract.portfolio.election.domain.ElectionStatus;
import org.apache.fineract.portfolio.election.domain.ElectionType;
import org.apache.fineract.portfolio.election.domain.ElectionVote;
import org.apache.fineract.portfolio.election.domain.ElectionVoteRepository;
import org.apache.fineract.portfolio.election.domain.NominationStatus;
import org.apache.fineract.portfolio.election.exception.ElectionAlreadyOngoingException;
import org.apache.fineract.portfolio.election.exception.ElectionNotFoundException;
import org.apache.fineract.portfolio.election.exception.ElectionNominationNotFoundException;
import org.apache.fineract.portfolio.election.exception.ElectionNotActiveException;
import org.apache.fineract.portfolio.election.exception.InvalidNominationStateException;
import org.apache.fineract.portfolio.election.exception.NominationAlreadyExistsException;
import org.apache.fineract.portfolio.election.exception.VoteAlreadyCastException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Slf4j
@Service
@RequiredArgsConstructor
public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformService {
private static final List<String> APPROVAL_STEPS = List.of(
"Director Review",
"Family Details Verification",
"Final Approval",
"CEO Final Sign-off"
);
private final PlatformSecurityContext context;
private final ElectionConfigRepository electionConfigRepository;
private final ElectionPostRepository electionPostRepository;
private final ElectionNominationRepository nominationRepository;
private final ElectionCandidateRepository candidateRepository;
private final ElectionApprovalStepRepository approvalStepRepository;
private final ElectionCeoPrecheckRepository ceoPrecheckRepository;
private final ElectionVoteRepository voteRepository;
private final ElectionResultRepository resultRepository;
private final ElectionEligibilityService eligibilityService;
private final ElectionCeoPreCheckService ceoPreCheckService;
private final ElectionNoDueCertificateService noDueCertificateService;
private static final Set<ElectionStatus> ONGOING_STATUSES = Set.of(
ElectionStatus.ACTIVE,
ElectionStatus.NOMINATION_CLOSED,
ElectionStatus.VOTING_OPEN,
ElectionStatus.VOTING_CLOSED
);
@Override
@Transactional
public Long createElection(ElectionConfigRequest request) {
context.authenticatedUser().validateHasPermissionTo("CREATE_ELECTION_CONFIG");
if (electionConfigRepository.existsByStatusIn(ONGOING_STATUSES)) {
throw new ElectionAlreadyOngoingException();
}
ElectionConfig election = ElectionConfig.create(
request.getTitle(),
request.getDescription(),
ElectionType.valueOf(request.getElectionType()),
request.getNominationStartDate(),
request.getNominationEndDate(),
request.getElectionDate(),
request.getResultDate()
);
ElectionConfig saved = electionConfigRepository.saveAndFlush(election);
if (request.getPosts() != null) {
for (ElectionPostRequest postReq : request.getPosts()) {
ElectionPost post = ElectionPost.create(saved, postReq.getPostName(), postReq.getTotalSeats());
electionPostRepository.saveAndFlush(post);
}
}
return saved.getId();
}
@Override
@Transactional
public void updateElection(Long electionId, ElectionConfigRequest request) {
context.authenticatedUser().validateHasPermissionTo("UPDATE_ELECTION");
ElectionConfig election = electionConfigRepository.findById(electionId)
.orElseThrow(() -> new ElectionNotFoundException(electionId));
if (request.getTitle() != null) election.setTitle(request.getTitle());
if (request.getDescription() != null) election.setDescription(request.getDescription());
if (request.getNominationStartDate() != null) election.setNominationStartDate(request.getNominationStartDate());
if (request.getNominationEndDate() != null) election.setNominationEndDate(request.getNominationEndDate());
if (request.getElectionDate() != null) election.setElectionDate(request.getElectionDate());
if (request.getResultDate() != null) election.setResultDate(request.getResultDate());
electionConfigRepository.save(election);
}
@Override
@Transactional
public void activateElection(Long electionId) {
context.authenticatedUser().validateHasPermissionTo("UPDATE_ELECTION");
ElectionConfig election = electionConfigRepository.findById(electionId)
.orElseThrow(() -> new ElectionNotFoundException(electionId));
election.activate();
electionConfigRepository.save(election);
}
@Override
@Transactional
public void closeNominations(Long electionId) {
context.authenticatedUser().validateHasPermissionTo("UPDATE_ELECTION");
ElectionConfig election = electionConfigRepository.findById(electionId)
.orElseThrow(() -> new ElectionNotFoundException(electionId));
election.closeNominations();
electionConfigRepository.save(election);
}
@Override
@Transactional
public void openVoting(Long electionId) {
context.authenticatedUser().validateHasPermissionTo("UPDATE_ELECTION");
ElectionConfig election = electionConfigRepository.findById(electionId)
.orElseThrow(() -> new ElectionNotFoundException(electionId));
election.openVoting();
electionConfigRepository.save(election);
}
@Override
@Transactional
public void closeVoting(Long electionId) {
context.authenticatedUser().validateHasPermissionTo("UPDATE_ELECTION");
ElectionConfig election = electionConfigRepository.findById(electionId)
.orElseThrow(() -> new ElectionNotFoundException(electionId));
election.closeVoting();
electionConfigRepository.save(election);
}
@Override
@Transactional
public void cancelElection(Long electionId, String reason) {
context.authenticatedUser().validateHasPermissionTo("UPDATE_ELECTION");
ElectionConfig election = electionConfigRepository.findById(electionId)
.orElseThrow(() -> new ElectionNotFoundException(electionId));
election.cancel(reason);
electionConfigRepository.save(election);
}
@Override
@Transactional
public Long submitNomination(ElectionNominationRequest request) {
ElectionConfig election = electionConfigRepository.findById(request.getElectionId())
.orElseThrow(() -> new ElectionNotFoundException(request.getElectionId()));
if (!election.isNominationOpen()) {
throw new ElectionNotActiveException(election.getId(), election.getStatus().name());
}
ElectionPost post = electionPostRepository.findById(request.getPostId())
.orElseThrow(() -> new IllegalArgumentException("Post not found: " + request.getPostId()));
if (nominationRepository.existsByElectionConfigIdAndMemberIdAndElectionPostId(
request.getElectionId(), request.getMemberId(), request.getPostId())) {
throw new NominationAlreadyExistsException(
request.getMemberId(), request.getElectionId(), request.getPostId());
}
ElectionNomination nomination = ElectionNomination.create(
election, post, request.getMemberId(), request.getShareAccountId(),
request.getApplicantName(), request.getDateOfBirth(), request.getGender(),
request.getCaste(), request.getCasteCategory(), request.getLocation(),
request.getServiceDetails(), request.getMobileNumber(), request.getEmail(),
request.getAddress(), request.getOccupation(), request.getShareCount()
);
nomination.setDeclarationAccepted(Boolean.TRUE.equals(request.getDeclarationAccepted()));
nomination.setPhotoDocumentId(request.getPhotoDocumentId());
ElectionNomination saved = nominationRepository.saveAndFlush(nomination);
String appNumber = generateApplicationNumber(saved.getId(), election.getId());
saved.submit(appNumber);
nominationRepository.saveAndFlush(saved);
return saved.getId();
}
@Override
@Transactional
public void withdrawNomination(Long nominationId) {
ElectionNomination nomination = nominationRepository.findById(nominationId)
.orElseThrow(() -> new ElectionNominationNotFoundException(nominationId));
if (nomination.getStatus() == NominationStatus.APPROVED) {
throw new InvalidNominationStateException("withdraw", nomination.getStatus().name());
}
nomination.withdraw();
nominationRepository.save(nomination);
}
@Override
@Transactional
public void runEligibilityCheck(Long nominationId) {
context.authenticatedUser().validateHasPermissionTo("ELIGIBILITY_CHECK_ELECTION");
ElectionNomination nomination = nominationRepository.findById(nominationId)
.orElseThrow(() -> new ElectionNominationNotFoundException(nominationId));
Long checkedBy = context.authenticatedUser().getId();
eligibilityService.performCheck(nomination, nomination.getElectionPost(), checkedBy);
nomination.markUnderReview();
nominationRepository.save(nomination);
}
@Override
@Transactional
public void runCeoPreCheck(Long nominationId) {
context.authenticatedUser().validateHasPermissionTo("CEO_PRECHECK_ELECTION");
ElectionNomination nomination = nominationRepository.findById(nominationId)
.orElseThrow(() -> new ElectionNominationNotFoundException(nominationId));
nomination.sendToCeoCheck();
nominationRepository.save(nomination);
ceoPreCheckService.performDirectorLoanCheck(nomination);
ceoPreCheckService.performRelationAccountCheck(nomination);
ceoPrecheckRepository.findByNominationId(nominationId).ifPresent(precheck -> {
precheck.finalise(context.authenticatedUser().getId(), "CEO pre-check completed");
ceoPrecheckRepository.save(precheck);
if (org.apache.fineract.portfolio.election.domain.CheckResult.PASS == precheck.getOverallResult()) {
nomination.ceoApprove();
} else {
nomination.ceoReject("Pre-check failed: " + precheck.getRemarks());
}
nominationRepository.save(nomination);
});
}
@Override
@Transactional
public String generateNoDueCertificate(Long nominationId) {
context.authenticatedUser().validateHasPermissionTo("CEO_PRECHECK_ELECTION");
ElectionNomination nomination = nominationRepository.findById(nominationId)
.orElseThrow(() -> new ElectionNominationNotFoundException(nominationId));
return ceoPreCheckService.generateNoDueCertificateNumber(nomination);
}
@Override
@Transactional
public void processApprovalStep(Long nominationId, ElectionApprovalActionRequest request) {
context.authenticatedUser().validateHasPermissionTo("APPROVE_ELECTION_NOMINATION");
ElectionNomination nomination = nominationRepository.findById(nominationId)
.orElseThrow(() -> new ElectionNominationNotFoundException(nominationId));
if (nomination.getStatus() == NominationStatus.CEO_APPROVED
&& nomination.getApprovalSteps().isEmpty()) {
initApprovalSteps(nomination);
}
ElectionApprovalStep currentStep = approvalStepRepository
.findNextPendingStep(nominationId)
.orElseThrow(() -> new InvalidNominationStateException("approve", "No pending steps"));
Long approverId = context.authenticatedUser().getId();
if ("APPROVE".equalsIgnoreCase(request.getAction())) {
currentStep.approve(approverId, request.getRemarks(),
Boolean.TRUE.equals(request.getFamilyDetailsVerified()),
request.getFamilyDetailsJson());
approvalStepRepository.save(currentStep);
boolean allApproved = !approvalStepRepository
.existsByNominationIdAndStatus(nominationId, ApprovalStepStatus.PENDING);
if (allApproved) {
nomination.approve();
} else {
nomination.startApprovalWorkflow();
}
} else {
currentStep.reject(approverId, request.getRemarks());
approvalStepRepository.save(currentStep);
nomination.reject(request.getRejectionReason() != null
? request.getRejectionReason() : request.getRemarks());
}
nominationRepository.save(nomination);
}
@Override
@Transactional
public Long approveCandidate(Long nominationId, Long approvedBy) {
context.authenticatedUser().validateHasPermissionTo("APPROVE_ELECTION_CANDIDATE");
ElectionNomination nomination = nominationRepository.findById(nominationId)
.orElseThrow(() -> new ElectionNominationNotFoundException(nominationId));
if (nomination.getStatus() != NominationStatus.APPROVED) {
throw new InvalidNominationStateException("approve candidate", nomination.getStatus().name());
}
Integer maxSerial = candidateRepository.findMaxSerialNumberByElectionAndPost(
nomination.getElectionConfig().getId(), nomination.getElectionPost().getId())
.orElse(0);
ElectionCandidate candidate = ElectionCandidate.create(
nomination.getElectionConfig(),
nomination,
nomination.getElectionPost(),
nomination.getApplicantName(),
maxSerial + 1,
approvedBy != null ? approvedBy : context.authenticatedUser().getId()
);
return candidateRepository.save(candidate).getId();
}
@Override
@Transactional
public void removeCandidate(Long candidateId, String reason) {
context.authenticatedUser().validateHasPermissionTo("APPROVE_ELECTION_CANDIDATE");
ElectionCandidate candidate = candidateRepository.findById(candidateId)
.orElseThrow(() -> new IllegalArgumentException("Candidate not found: " + candidateId));
candidate.deactivate();
candidateRepository.save(candidate);
}
@Override
@Transactional
public void castVote(ElectionVoteRequest request) {
ElectionConfig election = electionConfigRepository.findById(request.getElectionId())
.orElseThrow(() -> new ElectionNotFoundException(request.getElectionId()));
if (!election.isVotingOpen()) {
throw new ElectionNotActiveException(election.getId(), election.getStatus().name());
}
if (voteRepository.existsByElectionConfigIdAndElectionPostIdAndVoterMemberId(
request.getElectionId(), request.getPostId(), request.getVoterMemberId())) {
throw new VoteAlreadyCastException(request.getVoterMemberId(), request.getPostId());
}
ElectionPost post = electionPostRepository.findById(request.getPostId())
.orElseThrow(() -> new IllegalArgumentException("Post not found: " + request.getPostId()));
ElectionCandidate candidate = candidateRepository.findById(request.getCandidateId())
.orElseThrow(() -> new IllegalArgumentException("Candidate not found: " + request.getCandidateId()));
ElectionVote vote = ElectionVote.cast(election, post, request.getVoterMemberId(),
candidate, request.getBoothId(), null);
voteRepository.save(vote);
}
@Override
@Transactional
public void declareResults(Long electionId, Long declaredBy) {
context.authenticatedUser().validateHasPermissionTo("DECLARE_ELECTION_RESULT");
ElectionConfig election = electionConfigRepository.findById(electionId)
.orElseThrow(() -> new ElectionNotFoundException(electionId));
List<ElectionPost> posts = electionPostRepository.findByElectionConfigIdAndIsActiveTrue(electionId);
for (ElectionPost post : posts) {
List<Object[]> voteCounts = voteRepository.countVotesByCandidate(electionId, post.getId());
int rank = 1;
for (Object[] row : voteCounts) {
Long candidateId = (Long) row[0];
Long count = (Long) row[1];
ElectionCandidate candidate = candidateRepository.findById(candidateId).orElse(null);
if (candidate == null) continue;
String result = rank <= post.getTotalSeats() ? "WON" : "LOST";
ElectionResult r = ElectionResult.create(election, post, candidate,
count.intValue(), result, rank, declaredBy);
resultRepository.save(r);
rank++;
}
}
election.declareResult();
electionConfigRepository.save(election);
}
private void initApprovalSteps(ElectionNomination nomination) {
for (int i = 0; i < APPROVAL_STEPS.size(); i++) {
ElectionApprovalStep step = ElectionApprovalStep.create(
nomination, i + 1, APPROVAL_STEPS.get(i), "BOARD_DIRECTOR");
approvalStepRepository.save(step);
}
nomination.startApprovalWorkflow();
nominationRepository.save(nomination);
}
private String generateApplicationNumber(Long nominationId, Long electionId) {
return "ENOMN-" + electionId + "-" + String.format("%06d", nominationId);
}
}
@@ -364,4 +364,5 @@
<include file="parts/0334_clerk_disbursement_add_error_message.xml" relativeToChangelogFile="true"/>
<include file="parts/0335_create_bank_transfer_log.xml" relativeToChangelogFile="true"/>
<include file="parts/0336_add_request_type_to_clerk_disbursement.xml" relativeToChangelogFile="true"/>
<include file="parts/0337_create_election_module.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,390 @@
<?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">
<!-- 1. m_election_config -->
<changeSet id="0337_create_m_election_config" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_election_config"/></not>
</preConditions>
<createTable tableName="m_election_config">
<column name="id" type="BIGSERIAL" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="title" type="VARCHAR(200)">
<constraints nullable="false"/>
</column>
<column name="description" type="TEXT"/>
<column name="election_type" type="VARCHAR(50)">
<constraints nullable="false"/>
</column>
<column name="nomination_start_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="nomination_end_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="election_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="result_date" type="DATE"/>
<column name="status" type="VARCHAR(50)" defaultValue="DRAFT">
<constraints nullable="false"/>
</column>
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="total_seats" type="INT"/>
<column name="remarks" type="TEXT"/>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="TIMESTAMP"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="TIMESTAMP"/>
</createTable>
</changeSet>
<!-- 2. m_election_post -->
<changeSet id="0337_create_m_election_post" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_election_post"/></not>
</preConditions>
<createTable tableName="m_election_post">
<column name="id" type="BIGSERIAL" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="election_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_election_post_config" references="m_election_config(id)"/>
</column>
<column name="post_name" type="VARCHAR(200)">
<constraints nullable="false"/>
</column>
<column name="total_seats" type="INT" defaultValueNumeric="1">
<constraints nullable="false"/>
</column>
<column name="caste_category" type="VARCHAR(100)"/>
<column name="gender_type" type="VARCHAR(20)" defaultValue="ANY">
<constraints nullable="false"/>
</column>
<column name="min_age" type="INT"/>
<column name="max_age" type="INT"/>
<column name="min_share_holding" type="INT" defaultValueNumeric="0"/>
<column name="area" type="VARCHAR(200)"/>
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="description" type="TEXT"/>
</createTable>
</changeSet>
<!-- 3. m_election_nomination -->
<changeSet id="0337_create_m_election_nomination" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_election_nomination"/></not>
</preConditions>
<createTable tableName="m_election_nomination">
<column name="id" type="BIGSERIAL" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="election_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_nomination_election" references="m_election_config(id)"/>
</column>
<column name="post_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_nomination_post" references="m_election_post(id)"/>
</column>
<column name="member_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="share_account_id" type="BIGINT"/>
<column name="application_number" type="VARCHAR(50)">
<constraints unique="true"/>
</column>
<column name="applicant_name" type="VARCHAR(200)">
<constraints nullable="false"/>
</column>
<column name="date_of_birth" type="DATE"/>
<column name="gender" type="VARCHAR(20)"/>
<column name="caste" type="VARCHAR(100)"/>
<column name="caste_category" type="VARCHAR(100)"/>
<column name="location" type="VARCHAR(200)"/>
<column name="service_details" type="VARCHAR(500)"/>
<column name="mobile_number" type="VARCHAR(20)"/>
<column name="email" type="VARCHAR(150)"/>
<column name="address" type="TEXT"/>
<column name="occupation" type="VARCHAR(200)"/>
<column name="share_count" type="INT"/>
<column name="status" type="VARCHAR(50)" defaultValue="DRAFT">
<constraints nullable="false"/>
</column>
<column name="rejection_reason" type="TEXT"/>
<column name="submitted_date" type="TIMESTAMP"/>
<column name="declaration_accepted" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="photo_document_id" type="BIGINT"/>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="TIMESTAMP"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="TIMESTAMP"/>
</createTable>
<addUniqueConstraint tableName="m_election_nomination"
columnNames="election_id, member_id, post_id"
constraintName="uq_nomination_election_member_post"/>
</changeSet>
<!-- 4. m_election_eligibility_check -->
<changeSet id="0337_create_m_election_eligibility_check" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_election_eligibility_check"/></not>
</preConditions>
<createTable tableName="m_election_eligibility_check">
<column name="id" type="BIGSERIAL" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="nomination_id" type="BIGINT">
<constraints nullable="false" unique="true" foreignKeyName="fk_eligibility_nomination" references="m_election_nomination(id)"/>
</column>
<column name="age_eligible" type="BOOLEAN"/>
<column name="age_check_remarks" type="VARCHAR(500)"/>
<column name="gender_eligible" type="BOOLEAN"/>
<column name="gender_check_remarks" type="VARCHAR(500)"/>
<column name="caste_eligible" type="BOOLEAN"/>
<column name="caste_check_remarks" type="VARCHAR(500)"/>
<column name="share_holding_eligible" type="BOOLEAN"/>
<column name="share_check_remarks" type="VARCHAR(500)"/>
<column name="area_eligible" type="BOOLEAN"/>
<column name="area_check_remarks" type="VARCHAR(500)"/>
<column name="overall_result" type="VARCHAR(20)" defaultValue="PENDING"/>
<column name="checked_by" type="BIGINT"/>
<column name="checked_date" type="TIMESTAMP"/>
<column name="remarks" type="TEXT"/>
</createTable>
</changeSet>
<!-- 5. m_election_ceo_precheck -->
<changeSet id="0337_create_m_election_ceo_precheck" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_election_ceo_precheck"/></not>
</preConditions>
<createTable tableName="m_election_ceo_precheck">
<column name="id" type="BIGSERIAL" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="nomination_id" type="BIGINT">
<constraints nullable="false" unique="true" foreignKeyName="fk_ceoprecheck_nomination" references="m_election_nomination(id)"/>
</column>
<column name="director_loan_check_result" type="VARCHAR(20)" defaultValue="PENDING"/>
<column name="director_loan_outstanding" type="DECIMAL(19,4)" defaultValueNumeric="0"/>
<column name="director_loan_remarks" type="TEXT"/>
<column name="relation_ac_check_result" type="VARCHAR(20)" defaultValue="PENDING"/>
<column name="relation_accounts_details" type="TEXT"/>
<column name="relation_ac_remarks" type="TEXT"/>
<column name="no_due_check_result" type="VARCHAR(20)" defaultValue="PENDING"/>
<column name="no_due_certificate_number" type="VARCHAR(100)"/>
<column name="no_due_generated_date" type="TIMESTAMP"/>
<column name="overall_result" type="VARCHAR(20)" defaultValue="PENDING"/>
<column name="checked_by" type="BIGINT"/>
<column name="checked_date" type="TIMESTAMP"/>
<column name="remarks" type="TEXT"/>
</createTable>
</changeSet>
<!-- 6. m_election_approval_step -->
<changeSet id="0337_create_m_election_approval_step" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_election_approval_step"/></not>
</preConditions>
<createTable tableName="m_election_approval_step">
<column name="id" type="BIGSERIAL" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="nomination_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_approval_step_nomination" references="m_election_nomination(id)"/>
</column>
<column name="step_number" type="INT">
<constraints nullable="false"/>
</column>
<column name="step_name" type="VARCHAR(200)">
<constraints nullable="false"/>
</column>
<column name="approver_role" type="VARCHAR(100)"/>
<column name="approver_user_id" type="BIGINT"/>
<column name="status" type="VARCHAR(50)" defaultValue="PENDING">
<constraints nullable="false"/>
</column>
<column name="remarks" type="TEXT"/>
<column name="action_date" type="TIMESTAMP"/>
<column name="family_details_verified" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="family_details_json" type="TEXT"/>
</createTable>
</changeSet>
<!-- 7. m_election_candidate -->
<changeSet id="0337_create_m_election_candidate" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_election_candidate"/></not>
</preConditions>
<createTable tableName="m_election_candidate">
<column name="id" type="BIGSERIAL" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="election_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_candidate_election" references="m_election_config(id)"/>
</column>
<column name="nomination_id" type="BIGINT">
<constraints nullable="false" unique="true" foreignKeyName="fk_candidate_nomination" references="m_election_nomination(id)"/>
</column>
<column name="post_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_candidate_post" references="m_election_post(id)"/>
</column>
<column name="candidate_name" type="VARCHAR(200)">
<constraints nullable="false"/>
</column>
<column name="serial_number" type="INT"/>
<column name="photo_url" type="VARCHAR(500)"/>
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="approved_by" type="BIGINT"/>
<column name="approved_date" type="TIMESTAMP"/>
<column name="symbol" type="VARCHAR(100)"/>
<column name="brief_profile" type="TEXT"/>
</createTable>
</changeSet>
<!-- 8. m_election_vote -->
<changeSet id="0337_create_m_election_vote" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_election_vote"/></not>
</preConditions>
<createTable tableName="m_election_vote">
<column name="id" type="BIGSERIAL" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="election_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_vote_election" references="m_election_config(id)"/>
</column>
<column name="post_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_vote_post" references="m_election_post(id)"/>
</column>
<column name="voter_member_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="candidate_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_vote_candidate" references="m_election_candidate(id)"/>
</column>
<column name="vote_timestamp" type="TIMESTAMP">
<constraints nullable="false"/>
</column>
<column name="booth_id" type="VARCHAR(100)"/>
<column name="ip_address" type="VARCHAR(50)"/>
</createTable>
<addUniqueConstraint tableName="m_election_vote"
columnNames="election_id, post_id, voter_member_id"
constraintName="uq_election_vote_member_post"/>
</changeSet>
<!-- 9. m_election_result -->
<changeSet id="0337_create_m_election_result" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_election_result"/></not>
</preConditions>
<createTable tableName="m_election_result">
<column name="id" type="BIGSERIAL" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="election_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_result_election" references="m_election_config(id)"/>
</column>
<column name="post_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_result_post" references="m_election_post(id)"/>
</column>
<column name="candidate_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_result_candidate" references="m_election_candidate(id)"/>
</column>
<column name="vote_count" type="INT" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="result" type="VARCHAR(20)"/>
<column name="rank_position" type="INT"/>
<column name="declared_by" type="BIGINT"/>
<column name="declared_date" type="TIMESTAMP"/>
</createTable>
</changeSet>
<!-- 10. Permissions for election module -->
<changeSet id="0337_seed_election_permissions" author="CBS">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">
SELECT COUNT(*) FROM m_permission WHERE code = 'SIDEBAR_ELECTION'
</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="SIDEBAR_ELECTION"/>
<column name="entity_name" value="ELECTION"/>
<column name="action_name" value="SIDEBAR"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_ELECTION_CONFIG"/>
<column name="entity_name" value="ELECTION_CONFIG"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_ELECTION_CONFIG"/>
<column name="entity_name" value="ELECTION_CONFIG"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="UPDATE_ELECTION_CONFIG"/>
<column name="entity_name" value="ELECTION_CONFIG"/>
<column name="action_name" value="UPDATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_ELECTION_NOMINATION"/>
<column name="entity_name" value="ELECTION_NOMINATION"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_ELECTION_NOMINATION"/>
<column name="entity_name" value="ELECTION_NOMINATION"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_ELECTION_CANDIDATE"/>
<column name="entity_name" value="ELECTION_CANDIDATE"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CAST_ELECTION_VOTE"/>
<column name="entity_name" value="ELECTION_VOTE"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="DECLARE_ELECTION_RESULT"/>
<column name="entity_name" value="ELECTION_RESULT"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>