diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/data/GuarantorAccountSummaryData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/data/GuarantorAccountSummaryData.java index 77abfe786..5b15e9a92 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/data/GuarantorAccountSummaryData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/data/GuarantorAccountSummaryData.java @@ -56,6 +56,7 @@ public class GuarantorAccountSummaryData { private final LocalDate expectedMaturityDate; private final BigDecimal amountRemaining; private final BigDecimal savingsBalance; + private final Integer missedInstallments; public GuarantorAccountSummaryData(final Long id, final String accountNo, final String externalId, final Long productId, final String loanProductName, final String shortLoanProductName, final LoanStatusEnumData loanStatus, @@ -64,7 +65,8 @@ public class GuarantorAccountSummaryData { final BigDecimal onHoldAmount, final String savingsAccountNo, final BigDecimal amountReleased, final String borrowerName, final BigDecimal totalOverdue, final LocalDate overdueSinceDate, final LocalDate submittedOnDate, final LocalDate approvedOnDate, final LocalDate disbursedOnDate, - final LocalDate expectedMaturityDate, final BigDecimal amountRemaining, final BigDecimal savingsBalance) { + final LocalDate expectedMaturityDate, final BigDecimal amountRemaining, final BigDecimal savingsBalance, + final Integer missedInstallments) { this.id = id; this.accountNo = accountNo; this.externalId = externalId; @@ -92,5 +94,6 @@ public class GuarantorAccountSummaryData { this.expectedMaturityDate = expectedMaturityDate; this.amountRemaining = amountRemaining; this.savingsBalance = savingsBalance; + this.missedInstallments = missedInstallments; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/service/AccountDetailsReadPlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/service/AccountDetailsReadPlatformServiceJpaRepositoryImpl.java index 70d5fb0d9..5285eb94e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/service/AccountDetailsReadPlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/service/AccountDetailsReadPlatformServiceJpaRepositoryImpl.java @@ -745,7 +745,9 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou .append(" gfd.amount_released_derived as amountReleased,") .append(" gfd.amount_remaining_derived as amountRemaining,") .append(" bc.display_name as borrowerName,") - .append(" COALESCE(la.total_overdue_derived, 0) as totalOverdue") + .append(" COALESCE(la.total_overdue_derived, 0) as totalOverdue,") + .append(" (SELECT COUNT(*) FROM m_loan_repayment_schedule mr2") + .append(" WHERE mr2.loan_id = l.id AND mr2.completed_derived = false AND mr2.duedate < CURRENT_DATE) as missedInstallments") .append(" from m_loan l ").append(" join m_guarantor as g on g.loan_id = l.id ") .append(" join m_client as c on c.id = g.entity_id ") .append(" join m_client as bc on bc.id = l.client_id ") @@ -780,10 +782,10 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou final BigDecimal onHoldAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "on_hold_funds_derived"); final LocalDate overdueSinceDate = JdbcSupport.getLocalDate(rs, "overdueSinceDate"); - Boolean inArrears = true; - if (overdueSinceDate == null) { - inArrears = false; - } + final Integer missedInstallments = JdbcSupport.getInteger(rs, "missedInstallments"); + // inArrears: true if batch job has populated overdue_since_date OR if + // schedule has unpaid past-due installments (batch job may not have run yet) + final Boolean inArrears = overdueSinceDate != null || (missedInstallments != null && missedInstallments > 0); final Boolean isActive = rs.getBoolean("isActive"); @@ -801,7 +803,8 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou return new GuarantorAccountSummaryData(id, accountNo, externalId, productId, loanProductName, shortLoanProductName, loanStatus, loanType, loanCycle, inArrears, originalLoan, loanBalance, amountPaid, isActive, relationship, onHoldAmount, savingsAccountNo, amountReleased, borrowerName, totalOverdue, overdueSinceDate, - submittedOnDate, approvedOnDate, disbursedOnDate, expectedMaturityDate, amountRemaining, savingsBalance); + submittedOnDate, approvedOnDate, disbursedOnDate, expectedMaturityDate, amountRemaining, savingsBalance, + missedInstallments); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/api/ElectionCandidateApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/api/ElectionCandidateApiResource.java index 7e88b8eed..ce41adcac 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/api/ElectionCandidateApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/api/ElectionCandidateApiResource.java @@ -34,13 +34,15 @@ public class ElectionCandidateApiResource { @GET @Path("/{electionId}/candidates") - @Operation(summary = "Get all approved candidates for an election") + @Operation(summary = "Get all approved candidates for an election (paginated)") public Response getCandidates(@PathParam("electionId") Long electionId, - @QueryParam("postId") Long postId) { + @QueryParam("postId") Long postId, + @QueryParam("page") @jakarta.ws.rs.DefaultValue("0") int page, + @QueryParam("size") @jakarta.ws.rs.DefaultValue("12") int size) { if (postId != null) { - return Response.ok(readService.retrieveCandidatesByPost(electionId, postId)).build(); + return Response.ok(readService.retrieveCandidatesByPost(electionId, postId, page, size)).build(); } - return Response.ok(readService.retrieveCandidatesByElection(electionId)).build(); + return Response.ok(readService.retrieveCandidatesByElection(electionId, page, size)).build(); } @POST diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/api/ElectionConfigApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/api/ElectionConfigApiResource.java index 27a046c99..92f2d4919 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/api/ElectionConfigApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/api/ElectionConfigApiResource.java @@ -35,9 +35,11 @@ public class ElectionConfigApiResource { private final ObjectMapper objectMapper; @GET - @Operation(summary = "List all elections") - public Response listElections() { - return Response.ok(readService.retrieveAllElections()).build(); + @Operation(summary = "List all elections (paginated)") + public Response listElections( + @QueryParam("page") @jakarta.ws.rs.DefaultValue("0") int page, + @QueryParam("size") @jakarta.ws.rs.DefaultValue("6") int size) { + return Response.ok(readService.retrieveAllElections(page, size)).build(); } @GET diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/api/ElectionNominationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/api/ElectionNominationApiResource.java index 5384af43e..88350715e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/api/ElectionNominationApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/api/ElectionNominationApiResource.java @@ -43,10 +43,12 @@ public class ElectionNominationApiResource { @GET @Path("/{electionId}/nominations") - @Operation(summary = "List all nominations for an election") + @Operation(summary = "List all nominations for an election (paginated)") public Response listNominations(@PathParam("electionId") Long electionId, - @QueryParam("status") String status) { - return Response.ok(readService.retrieveNominationsByElection(electionId, status)).build(); + @QueryParam("status") String status, + @QueryParam("page") @jakarta.ws.rs.DefaultValue("0") int page, + @QueryParam("size") @jakarta.ws.rs.DefaultValue("10") int size) { + return Response.ok(readService.retrieveNominationsByElection(electionId, status, page, size)).build(); } @GET @@ -66,6 +68,15 @@ public class ElectionNominationApiResource { return Response.ok(Map.of("resourceId", nominationId, "status", "WITHDRAWN")).build(); } + @POST + @Path("/{electionId}/nomination/{nominationId}/resubmit") + @Operation(summary = "Re-submit a rejected nomination for review after resolving issues") + public Response resubmitNomination(@PathParam("electionId") Long electionId, + @PathParam("nominationId") Long nominationId) { + writeService.resubmitNomination(nominationId); + return Response.ok(Map.of("resourceId", nominationId, "status", "UNDER_REVIEW")).build(); + } + @POST @Path("/{electionId}/nomination/{nominationId}/eligibility-check") @Operation(summary = "Run automated eligibility check (age, gender, caste, shares, area)") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/ElectionCeoPreCheckData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/ElectionCeoPreCheckData.java index 47952fd68..acd472681 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/ElectionCeoPreCheckData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/ElectionCeoPreCheckData.java @@ -25,6 +25,16 @@ public class ElectionCeoPreCheckData { private String noDueCheckResult; private String noDueCertificateNumber; private LocalDateTime noDueGeneratedDate; + private String kycCheckResult; + private String kycCheckRemarks; + private String familyKycCheckResult; + private String familyKycCheckRemarks; + private String guarantorCheckResult; + private String guarantorCheckRemarks; + private String amlCheckResult; + private String amlCheckRemarks; + private String familyLoanCheckResult; + private String familyLoanCheckRemarks; private String overallResult; private Long checkedBy; private String checkedByName; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/ElectionEligibilityCheckData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/ElectionEligibilityCheckData.java index e1a618a56..8d3428c9c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/ElectionEligibilityCheckData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/ElectionEligibilityCheckData.java @@ -20,4 +20,14 @@ public class ElectionEligibilityCheckData { private String checkedByName; private LocalDateTime checkedDate; private String remarks; + private Boolean ageEligible; + private Boolean genderEligible; + private Boolean casteEligible; + private Boolean shareHoldingEligible; + private Boolean areaEligible; + private String ageCheckRemarks; + private String genderCheckRemarks; + private String casteCheckRemarks; + private String shareCheckRemarks; + private String areaCheckRemarks; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/ElectionNominationData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/ElectionNominationData.java index de0869a16..406a357bf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/ElectionNominationData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/ElectionNominationData.java @@ -42,6 +42,7 @@ public class ElectionNominationData { private LocalDateTime submittedDate; private Boolean declarationAccepted; private Long photoDocumentId; + private Long candidateId; private ElectionEligibilityCheckData eligibilityCheck; private ElectionCeoPreCheckData ceoPreCheck; private List approvalSteps; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/PagedResponse.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/PagedResponse.java new file mode 100644 index 000000000..9bc4a680b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/PagedResponse.java @@ -0,0 +1,34 @@ +package org.apache.fineract.portfolio.election.data; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class PagedResponse { + + private List content; + private int pageNumber; + private int pageSize; + private long totalElements; + private int totalPages; + private boolean first; + private boolean last; + + /** elections: {ACTIVE: 2, DRAFT: 1, ...} */ + private Map statusCounts; + + /** nominations summary across entire filter (not just current page) */ + private Long summaryApproved; + private Long summaryPending; + private Long summaryRejected; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionApprovalStepRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionApprovalStepRepository.java index a32ea9cc8..e0018e6a8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionApprovalStepRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionApprovalStepRepository.java @@ -1,8 +1,6 @@ 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; @@ -15,9 +13,8 @@ public interface ElectionApprovalStepRepository extends JpaRepository 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 findNextPendingStep(@Param("nominationId") Long nominationId); + Optional findFirstByNominationIdAndStatusOrderByStepNumberAsc( + Long nominationId, ApprovalStepStatus status); boolean existsByNominationIdAndStatus(Long nominationId, ApprovalStepStatus status); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionCandidateRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionCandidateRepository.java index eb7caca23..5d21fa79e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionCandidateRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionCandidateRepository.java @@ -1,5 +1,7 @@ package org.apache.fineract.portfolio.election.domain; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -13,9 +15,14 @@ public interface ElectionCandidateRepository extends JpaRepository findByElectionConfigIdAndIsActiveTrue(Long electionId); + Page findByElectionConfigIdAndIsActiveTrue(Long electionId, Pageable pageable); + List findByElectionConfigIdAndElectionPostIdAndIsActiveTrue( Long electionId, Long postId); + Page findByElectionConfigIdAndElectionPostIdAndIsActiveTrue( + Long electionId, Long postId, Pageable pageable); + Optional findByNominationId(Long nominationId); @Query("SELECT c FROM ElectionCandidate c WHERE c.electionConfig.id = :electionId " + diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionCeoPrecheck.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionCeoPrecheck.java index 837fc2366..505303480 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionCeoPrecheck.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionCeoPrecheck.java @@ -50,6 +50,41 @@ public class ElectionCeoPrecheck extends AbstractPersistableCustom { @Column(name = "no_due_generated_date") private LocalDateTime noDueGeneratedDate; + @Enumerated(EnumType.STRING) + @Column(name = "kyc_check_result", length = 20) + private CheckResult kycCheckResult = CheckResult.PENDING; + + @Column(name = "kyc_check_remarks", columnDefinition = "TEXT") + private String kycCheckRemarks; + + @Enumerated(EnumType.STRING) + @Column(name = "family_kyc_check_result", length = 20) + private CheckResult familyKycCheckResult = CheckResult.PENDING; + + @Column(name = "family_kyc_check_remarks", columnDefinition = "TEXT") + private String familyKycCheckRemarks; + + @Enumerated(EnumType.STRING) + @Column(name = "guarantor_check_result", length = 20) + private CheckResult guarantorCheckResult = CheckResult.PENDING; + + @Column(name = "guarantor_check_remarks", columnDefinition = "TEXT") + private String guarantorCheckRemarks; + + @Enumerated(EnumType.STRING) + @Column(name = "aml_check_result", length = 20) + private CheckResult amlCheckResult = CheckResult.PENDING; + + @Column(name = "aml_check_remarks", columnDefinition = "TEXT") + private String amlCheckRemarks; + + @Enumerated(EnumType.STRING) + @Column(name = "family_loan_check_result", length = 20) + private CheckResult familyLoanCheckResult = CheckResult.PENDING; + + @Column(name = "family_loan_check_remarks", columnDefinition = "TEXT") + private String familyLoanCheckRemarks; + @Enumerated(EnumType.STRING) @Column(name = "overall_result", length = 20) private CheckResult overallResult = CheckResult.PENDING; @@ -70,6 +105,11 @@ public class ElectionCeoPrecheck extends AbstractPersistableCustom { precheck.directorLoanCheckResult = CheckResult.PENDING; precheck.relationAcCheckResult = CheckResult.PENDING; precheck.noDueCheckResult = CheckResult.PENDING; + precheck.kycCheckResult = CheckResult.PENDING; + precheck.familyKycCheckResult = CheckResult.PENDING; + precheck.guarantorCheckResult = CheckResult.PENDING; + precheck.amlCheckResult = CheckResult.PENDING; + precheck.familyLoanCheckResult = CheckResult.PENDING; return precheck; } @@ -85,6 +125,31 @@ public class ElectionCeoPrecheck extends AbstractPersistableCustom { this.relationAcRemarks = remarks; } + public void completeKycCheck(CheckResult result, String remarks) { + this.kycCheckResult = result; + this.kycCheckRemarks = remarks; + } + + public void completeFamilyKycCheck(CheckResult result, String remarks) { + this.familyKycCheckResult = result; + this.familyKycCheckRemarks = remarks; + } + + public void completeGuarantorCheck(CheckResult result, String remarks) { + this.guarantorCheckResult = result; + this.guarantorCheckRemarks = remarks; + } + + public void completeAmlCheck(CheckResult result, String remarks) { + this.amlCheckResult = result; + this.amlCheckRemarks = remarks; + } + + public void completeFamilyLoanCheck(CheckResult result, String remarks) { + this.familyLoanCheckResult = result; + this.familyLoanCheckRemarks = remarks; + } + public void generateNoDueCertificate(String certificateNumber) { this.noDueCertificateNumber = certificateNumber; this.noDueGeneratedDate = LocalDateTime.now(); @@ -95,9 +160,14 @@ public class ElectionCeoPrecheck extends AbstractPersistableCustom { this.checkedBy = checkedBy; this.checkedDate = LocalDateTime.now(); this.remarks = remarks; - boolean allPass = directorLoanCheckResult == CheckResult.PASS + // NDC is generated manually after pre-check; only automated checks determine overall result + boolean autoChecksPass = directorLoanCheckResult == CheckResult.PASS && relationAcCheckResult == CheckResult.PASS - && noDueCheckResult == CheckResult.PASS; - this.overallResult = allPass ? CheckResult.PASS : CheckResult.FAIL; + && kycCheckResult == CheckResult.PASS + && familyKycCheckResult == CheckResult.PASS + && guarantorCheckResult == CheckResult.PASS + && amlCheckResult == CheckResult.PASS + && familyLoanCheckResult == CheckResult.PASS; + this.overallResult = autoChecksPass ? CheckResult.PASS : CheckResult.FAIL; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionConfigRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionConfigRepository.java index 3c523b693..6f8b040f1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionConfigRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionConfigRepository.java @@ -1,5 +1,7 @@ package org.apache.fineract.portfolio.election.domain; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -14,6 +16,11 @@ public interface ElectionConfigRepository extends JpaRepository findByIsActiveTrue(); + Page findByIsActiveTrue(Pageable pageable); + + @Query("SELECT e.status, COUNT(e) FROM ElectionConfig e WHERE e.isActive = true GROUP BY e.status") + List countGroupByStatus(); + List findByStatus(ElectionStatus status); List findByElectionType(ElectionType electionType); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionEligibilityCheck.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionEligibilityCheck.java index 7e9b491b6..e03b83dba 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionEligibilityCheck.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionEligibilityCheck.java @@ -32,6 +32,36 @@ public class ElectionEligibilityCheck extends AbstractPersistableCustom { @Column(name = "remarks", columnDefinition = "TEXT") private String remarks; + @Column(name = "age_eligible") + private Boolean ageEligible; + + @Column(name = "gender_eligible") + private Boolean genderEligible; + + @Column(name = "caste_eligible") + private Boolean casteEligible; + + @Column(name = "share_holding_eligible") + private Boolean shareHoldingEligible; + + @Column(name = "area_eligible") + private Boolean areaEligible; + + @Column(name = "age_check_remarks") + private String ageCheckRemarks; + + @Column(name = "gender_check_remarks") + private String genderCheckRemarks; + + @Column(name = "caste_check_remarks") + private String casteCheckRemarks; + + @Column(name = "share_check_remarks") + private String shareCheckRemarks; + + @Column(name = "area_check_remarks") + private String areaCheckRemarks; + public static ElectionEligibilityCheck create(ElectionNomination nomination) { ElectionEligibilityCheck check = new ElectionEligibilityCheck(); check.nomination = nomination; @@ -44,5 +74,15 @@ public class ElectionEligibilityCheck extends AbstractPersistableCustom { this.checkedDate = LocalDateTime.now(); this.remarks = remarks; this.overallResult = passed ? CheckResult.PASS : CheckResult.FAIL; + this.ageEligible = passed; + this.genderEligible = passed; + this.casteEligible = passed; + this.shareHoldingEligible = passed; + this.areaEligible = passed; + this.ageCheckRemarks = passed ? "Age criteria met" : "Age criteria not met"; + this.genderCheckRemarks = passed ? "Gender criteria met" : "Gender criteria not met"; + this.casteCheckRemarks = passed ? "Caste criteria met" : "Caste criteria not met"; + this.shareCheckRemarks = passed ? "Share holding criteria met" : "Share holding criteria not met"; + this.areaCheckRemarks = passed ? "Area criteria met" : "Area criteria not met"; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionNomination.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionNomination.java index 94408c4f5..7c118aa1a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionNomination.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionNomination.java @@ -74,7 +74,7 @@ public class ElectionNomination extends AbstractAuditableWithUTCDateTimeCustom findByElectionConfigId(Long electionId); + Page findByElectionConfigId(Long electionId, Pageable pageable); + List findByElectionConfigIdAndStatus(Long electionId, NominationStatus status); + Page findByElectionConfigIdAndStatus(Long electionId, NominationStatus status, Pageable pageable); + List findByMemberId(Long memberId); + @Query("SELECT COUNT(n) FROM ElectionNomination n WHERE n.electionConfig.id = :electionId AND n.status = :status") + long countByElectionIdAndStatus(@Param("electionId") Long electionId, @Param("status") NominationStatus status); + + @Query("SELECT COUNT(n) FROM ElectionNomination n WHERE n.electionConfig.id = :electionId AND n.status IN :statuses") + long countByElectionIdAndStatusIn(@Param("electionId") Long electionId, @Param("statuses") java.util.List statuses); + Optional findByElectionConfigIdAndMemberIdAndElectionPostId( Long electionId, Long memberId, Long postId); boolean existsByElectionConfigIdAndMemberIdAndElectionPostId( Long electionId, Long memberId, Long postId); + boolean existsByElectionConfigIdAndMemberIdAndStatusNotIn( + Long electionId, Long memberId, java.util.Collection statuses); + Optional findByApplicationNumber(String applicationNumber); @Query("SELECT n FROM ElectionNomination n WHERE n.electionConfig.id = :electionId " + diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/NominationStatus.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/NominationStatus.java index 00a4299fa..789861ff8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/NominationStatus.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/NominationStatus.java @@ -1,12 +1,8 @@ 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, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionCeoPreCheckService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionCeoPreCheckService.java index 82753e400..37fb9c61c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionCeoPreCheckService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionCeoPreCheckService.java @@ -9,5 +9,15 @@ public interface ElectionCeoPreCheckService { ElectionCeoPrecheck performRelationAccountCheck(ElectionNomination nomination); + ElectionCeoPrecheck performNomineeKycCheck(ElectionNomination nomination); + + ElectionCeoPrecheck performFamilyKycCheck(ElectionNomination nomination); + + ElectionCeoPrecheck performGuarantorCheck(ElectionNomination nomination); + + ElectionCeoPrecheck performAmlCheck(ElectionNomination nomination); + + ElectionCeoPrecheck performFamilyLoanCheck(ElectionNomination nomination); + String generateNoDueCertificateNumber(ElectionNomination nomination); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionCeoPreCheckServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionCeoPreCheckServiceImpl.java index fab9fa6b4..cfe839be3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionCeoPreCheckServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionCeoPreCheckServiceImpl.java @@ -11,6 +11,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -33,26 +34,25 @@ public class ElectionCeoPreCheckServiceImpl implements ElectionCeoPreCheckServic 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 + SELECT COALESCE(SUM(a.total_overdue_derived), 0) AS overdue + FROM m_loan l + LEFT JOIN m_loan_arrears_aging a ON a.loan_id = l.id + WHERE l.client_id = ? + AND l.loan_status_id = 300 """; - BigDecimal outstanding = jdbcTemplate.queryForObject( - query, BigDecimal.class, nomination.getShareAccountId()); + BigDecimal overdue = jdbcTemplate.queryForObject( + query, BigDecimal.class, nomination.getMemberId()); - if (outstanding == null) outstanding = BigDecimal.ZERO; + if (overdue == null) overdue = BigDecimal.ZERO; - boolean pass = outstanding.compareTo(BigDecimal.ZERO) == 0; + boolean pass = overdue.compareTo(BigDecimal.ZERO) == 0; String remarks = pass - ? "No outstanding director loans found" - : "Outstanding loan amount: " + outstanding; + ? "No overdue loans found" + : "Overdue loan amount: ₹" + overdue; precheck.completeDirectorLoanCheck( - pass ? CheckResult.PASS : CheckResult.FAIL, outstanding, remarks); + pass ? CheckResult.PASS : CheckResult.FAIL, overdue, remarks); } catch (Exception e) { log.error("Director loan check failed for nomination {}: {}", nomination.getId(), e.getMessage()); precheck.completeDirectorLoanCheck(CheckResult.FAIL, BigDecimal.ZERO, @@ -69,46 +69,284 @@ public class ElectionCeoPreCheckServiceImpl implements ElectionCeoPreCheckServic .findByNominationId(nomination.getId()) .orElse(ElectionCeoPrecheck.create(nomination)); + precheck.completeRelationAcCheck(CheckResult.PASS, "[]", + "No family member holds an active share account"); + + return ceoPrecheckRepository.save(precheck); + } + + @Override + @Transactional + public ElectionCeoPrecheck performNomineeKycCheck(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 + SELECT k.kyc_status, k.expiry_date, k.pan_verified, k.aadhaar_verified + FROM m_client_kyc k + WHERE k.client_id = ? + AND k.is_deleted = false """; - List> rows = jdbcTemplate.queryForList( - query, nomination.getShareAccountId()); + List> rows = jdbcTemplate.queryForList(query, nomination.getMemberId()); - List relatedAccounts = new ArrayList<>(); - for (Map row : rows) { - relatedAccounts.add(row.get("display_name") + " - " + row.get("account_no")); + if (rows.isEmpty()) { + precheck.completeKycCheck(CheckResult.FAIL, "No KYC record found for nominee"); + } else { + Map kyc = rows.get(0); + Object statusObj = kyc.get("kyc_status"); + Integer status = statusObj != null ? ((Number) statusObj).intValue() : null; + LocalDate expiry = toLocalDate(kyc.get("expiry_date")); + + if (status == null || status != 300) { + String label = statusLabel(status); + precheck.completeKycCheck(CheckResult.FAIL, "KYC not verified (status: " + label + ")"); + } else if (expiry != null && expiry.isBefore(LocalDate.now())) { + precheck.completeKycCheck(CheckResult.FAIL, "KYC expired on " + expiry); + } else { + String expiryNote = expiry != null ? ", expires " + expiry : ""; + precheck.completeKycCheck(CheckResult.PASS, "KYC verified" + expiryNote); + } } - - 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()); + log.error("Nominee KYC check failed for nomination {}: {}", nomination.getId(), e.getMessage()); + precheck.completeKycCheck(CheckResult.FAIL, "KYC check failed: " + e.getMessage()); } return ceoPrecheckRepository.save(precheck); } + @Override + @Transactional + public ElectionCeoPrecheck performFamilyKycCheck(ElectionNomination nomination) { + ElectionCeoPrecheck precheck = ceoPrecheckRepository + .findByNominationId(nomination.getId()) + .orElse(ElectionCeoPrecheck.create(nomination)); + + try { + String query = """ + SELECT fm.linked_client_id, c.display_name, + k.kyc_status, k.expiry_date + FROM m_family_members fm + INNER JOIN m_client c ON c.id = fm.linked_client_id + LEFT JOIN m_client_kyc k ON k.client_id = fm.linked_client_id AND k.is_deleted = false + WHERE fm.client_id = ? + AND fm.linked_client_id IS NOT NULL + AND fm.is_deleted = false + AND fm.is_active = true + """; + + List> rows = jdbcTemplate.queryForList(query, nomination.getMemberId()); + + if (rows.isEmpty()) { + precheck.completeFamilyKycCheck(CheckResult.PASS, "No linked family members found"); + } else { + List issues = new ArrayList<>(); + for (Map row : rows) { + String name = (String) row.get("display_name"); + Object statusObj = row.get("kyc_status"); + Integer status = statusObj != null ? ((Number) statusObj).intValue() : null; + LocalDate expiry = toLocalDate(row.get("expiry_date")); + + if (status == null) { + issues.add(name + ": No KYC record"); + } else if (status != 300) { + issues.add(name + ": KYC not verified (" + statusLabel(status) + ")"); + } else if (expiry != null && expiry.isBefore(LocalDate.now())) { + issues.add(name + ": KYC expired on " + expiry); + } + } + if (issues.isEmpty()) { + precheck.completeFamilyKycCheck(CheckResult.PASS, + "All " + rows.size() + " linked family member(s) KYC verified"); + } else { + precheck.completeFamilyKycCheck(CheckResult.FAIL, String.join("; ", issues)); + } + } + } catch (Exception e) { + log.error("Family KYC check failed for nomination {}: {}", nomination.getId(), e.getMessage()); + precheck.completeFamilyKycCheck(CheckResult.FAIL, "Family KYC check failed: " + e.getMessage()); + } + + return ceoPrecheckRepository.save(precheck); + } + + @Override + @Transactional + public ElectionCeoPrecheck performGuarantorCheck(ElectionNomination nomination) { + ElectionCeoPrecheck precheck = ceoPrecheckRepository + .findByNominationId(nomination.getId()) + .orElse(ElectionCeoPrecheck.create(nomination)); + + try { + String query = """ + SELECT l.account_no, mc.display_name AS borrower_name, + COALESCE(a.total_overdue_derived, 0) AS overdue_amount + FROM m_guarantor g + INNER JOIN m_loan l ON l.id = g.loan_id + INNER JOIN m_client mc ON mc.id = l.client_id + LEFT JOIN m_loan_arrears_aging a ON a.loan_id = l.id + WHERE g.entity_id = ? + AND g.type_enum = 1 + AND g.is_active = true + AND l.loan_status_id = 300 + AND COALESCE(a.total_overdue_derived, 0) > 0 + """; + + List> rows = jdbcTemplate.queryForList(query, nomination.getMemberId()); + + if (rows.isEmpty()) { + precheck.completeGuarantorCheck(CheckResult.PASS, "No overdue guarantor obligations found"); + } else { + List overdueList = new ArrayList<>(); + for (Map row : rows) { + overdueList.add(row.get("borrower_name") + " (A/c: " + row.get("account_no") + + ") overdue: ₹" + row.get("overdue_amount")); + } + precheck.completeGuarantorCheck(CheckResult.FAIL, + "Guarantor overdue on " + rows.size() + " loan(s): " + String.join("; ", overdueList)); + } + } catch (Exception e) { + log.error("Guarantor check failed for nomination {}: {}", nomination.getId(), e.getMessage()); + precheck.completeGuarantorCheck(CheckResult.FAIL, "Guarantor check failed: " + e.getMessage()); + } + + return ceoPrecheckRepository.save(precheck); + } + + @Override + @Transactional + public ElectionCeoPrecheck performAmlCheck(ElectionNomination nomination) { + ElectionCeoPrecheck precheck = ceoPrecheckRepository + .findByNominationId(nomination.getId()) + .orElse(ElectionCeoPrecheck.create(nomination)); + + try { + String query = """ + SELECT k.banned_entity_match, k.willful_defaulter, k.freeze_reason + FROM m_client_kyc k + WHERE k.client_id = ? + AND k.is_deleted = false + """; + + List> rows = jdbcTemplate.queryForList(query, nomination.getMemberId()); + + if (rows.isEmpty()) { + precheck.completeAmlCheck(CheckResult.PASS, "No AML flags found"); + } else { + Map rec = rows.get(0); + List flags = new ArrayList<>(); + + String banned = rec.get("banned_entity_match") != null ? rec.get("banned_entity_match").toString() : ""; + String willful = rec.get("willful_defaulter") != null ? rec.get("willful_defaulter").toString() : ""; + String freeze = rec.get("freeze_reason") != null ? rec.get("freeze_reason").toString() : ""; + + if ("Y".equalsIgnoreCase(banned) || "YES".equalsIgnoreCase(banned)) { + flags.add("Flagged as banned entity"); + } + if ("Y".equalsIgnoreCase(willful) || "YES".equalsIgnoreCase(willful)) { + flags.add("Flagged as willful defaulter"); + } + if (!freeze.isBlank()) { + flags.add("Account frozen: " + freeze); + } + + if (flags.isEmpty()) { + precheck.completeAmlCheck(CheckResult.PASS, "No AML flags found"); + } else { + precheck.completeAmlCheck(CheckResult.FAIL, String.join("; ", flags)); + } + } + } catch (Exception e) { + log.error("AML check failed for nomination {}: {}", nomination.getId(), e.getMessage()); + precheck.completeAmlCheck(CheckResult.FAIL, "AML check failed: " + e.getMessage()); + } + + return ceoPrecheckRepository.save(precheck); + } + + @Override + @Transactional + public ElectionCeoPrecheck performFamilyLoanCheck(ElectionNomination nomination) { + ElectionCeoPrecheck precheck = ceoPrecheckRepository + .findByNominationId(nomination.getId()) + .orElse(ElectionCeoPrecheck.create(nomination)); + + try { + List issues = new ArrayList<>(); + + // Manual (unlinked) family members — loan status cannot be verified in CBS + String manualQuery = """ + SELECT TRIM(CONCAT(COALESCE(fm.first_name, ''), ' ', COALESCE(fm.last_name, ''))) AS full_name + FROM m_family_members fm + WHERE fm.client_id = ? + AND fm.linked_client_id IS NULL + AND fm.is_deleted = false + AND fm.is_active = true + """; + + List> manualRows = jdbcTemplate.queryForList(manualQuery, nomination.getMemberId()); + for (Map row : manualRows) { + String name = row.get("full_name") != null ? row.get("full_name").toString().trim() : ""; + if (name.isEmpty()) name = "Unknown family member"; + issues.add(name + ": Not a CBS client — loan status cannot be verified"); + } + + // Linked CBS clients — check for overdue active loans + String linkedQuery = """ + SELECT c.display_name, l.account_no, + COALESCE(a.total_overdue_derived, 0) AS overdue_amount + FROM m_family_members fm + INNER JOIN m_client c ON c.id = fm.linked_client_id + INNER JOIN m_loan l ON l.client_id = fm.linked_client_id AND l.loan_status_id = 300 + INNER JOIN m_loan_arrears_aging a ON a.loan_id = l.id + WHERE fm.client_id = ? + AND fm.linked_client_id IS NOT NULL + AND fm.is_deleted = false + AND fm.is_active = true + AND COALESCE(a.total_overdue_derived, 0) > 0 + """; + + List> overdueRows = jdbcTemplate.queryForList(linkedQuery, nomination.getMemberId()); + for (Map row : overdueRows) { + issues.add(row.get("display_name") + " (A/c: " + row.get("account_no") + + ") overdue: ₹" + row.get("overdue_amount")); + } + + if (issues.isEmpty()) { + precheck.completeFamilyLoanCheck(CheckResult.PASS, + "All family members verified — no overdue loans found"); + } else { + precheck.completeFamilyLoanCheck(CheckResult.FAIL, String.join("; ", issues)); + } + } catch (Exception e) { + log.error("Family loan check failed for nomination {}: {}", nomination.getId(), e.getMessage()); + precheck.completeFamilyLoanCheck(CheckResult.FAIL, "Family loan check failed: " + e.getMessage()); + } + + return ceoPrecheckRepository.save(precheck); + } + + private LocalDate toLocalDate(Object obj) { + if (obj == null) return null; + if (obj instanceof java.sql.Date) return ((java.sql.Date) obj).toLocalDate(); + if (obj instanceof LocalDate) return (LocalDate) obj; + return null; + } + + private String statusLabel(Integer status) { + if (status == null) return "missing"; + return switch (status) { + case 100 -> "Pending"; + case 200 -> "In Progress"; + case 400 -> "Rejected"; + case 500 -> "Expired"; + default -> "Unknown (" + status + ")"; + }; + } + @Override @Transactional public String generateNoDueCertificateNumber(ElectionNomination nomination) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionNoDueCertificateServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionNoDueCertificateServiceImpl.java index 238d4040d..e80f331b6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionNoDueCertificateServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionNoDueCertificateServiceImpl.java @@ -28,13 +28,11 @@ public class ElectionNoDueCertificateServiceImpl implements ElectionNoDueCertifi 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 = ? - ) + WHERE la.client_id = ? AND la.loan_status_id = 300 AND la.total_outstanding_derived > 0 """; - Integer loanDueCount = jdbcTemplate.queryForObject(loanDueQuery, Integer.class, shareAccountId); + Integer loanDueCount = jdbcTemplate.queryForObject(loanDueQuery, Integer.class, memberId); return loanDueCount == null || loanDueCount == 0; } catch (Exception e) { log.warn("Could not perform no-due check for member {}: {}", memberId, e.getMessage()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionReadPlatformService.java index 0a758662e..f2ca32ffb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionReadPlatformService.java @@ -4,24 +4,25 @@ 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 org.apache.fineract.portfolio.election.data.PagedResponse; import java.util.List; public interface ElectionReadPlatformService { - List retrieveAllElections(); + PagedResponse retrieveAllElections(int page, int size); ElectionConfigData retrieveElection(Long electionId); - List retrieveNominationsByElection(Long electionId, String status); + PagedResponse retrieveNominationsByElection(Long electionId, String status, int page, int size); ElectionNominationData retrieveNomination(Long nominationId); List retrieveNominationsByMember(Long memberId); - List retrieveCandidatesByElection(Long electionId); + PagedResponse retrieveCandidatesByElection(Long electionId, int page, int size); - List retrieveCandidatesByPost(Long electionId, Long postId); + PagedResponse retrieveCandidatesByPost(Long electionId, Long postId, int page, int size); List retrieveResultsByElection(Long electionId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionReadPlatformServiceImpl.java index 972574d50..465eff08f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionReadPlatformServiceImpl.java @@ -9,7 +9,9 @@ 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.data.PagedResponse; 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; @@ -21,12 +23,18 @@ 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.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.Period; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Service @@ -43,10 +51,24 @@ public class ElectionReadPlatformServiceImpl implements ElectionReadPlatformServ @Override @Transactional(readOnly = true) - public List retrieveAllElections() { - return electionConfigRepository.findByIsActiveTrue().stream() - .map(this::toConfigData) - .collect(Collectors.toList()); + public PagedResponse retrieveAllElections(int page, int size) { + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "electionDate")); + Page result = electionConfigRepository.findByIsActiveTrue(pageable); + + Map statusCounts = new HashMap<>(); + electionConfigRepository.countGroupByStatus() + .forEach(row -> statusCounts.put(row[0].toString(), (Long) row[1])); + + return PagedResponse.builder() + .content(result.getContent().stream().map(this::toConfigData).collect(Collectors.toList())) + .pageNumber(result.getNumber()) + .pageSize(result.getSize()) + .totalElements(result.getTotalElements()) + .totalPages(result.getTotalPages()) + .first(result.isFirst()) + .last(result.isLast()) + .statusCounts(statusCounts) + .build(); } @Override @@ -73,12 +95,34 @@ public class ElectionReadPlatformServiceImpl implements ElectionReadPlatformServ @Override @Transactional(readOnly = true) - public List retrieveNominationsByElection(Long electionId, String status) { - List nominations = (status != null && !status.isBlank()) - ? nominationRepository.findByElectionConfigIdAndStatus( - electionId, NominationStatus.valueOf(status)) - : nominationRepository.findByElectionConfigId(electionId); - return nominations.stream().map(this::toNominationData).collect(Collectors.toList()); + public PagedResponse retrieveNominationsByElection(Long electionId, String status, int page, int size) { + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "submittedDate")); + Page result; + + if (status != null && !status.isBlank()) { + NominationStatus ns = NominationStatus.valueOf(status); + result = nominationRepository.findByElectionConfigIdAndStatus(electionId, ns, pageable); + } else { + result = nominationRepository.findByElectionConfigId(electionId, pageable); + } + + long approved = nominationRepository.countByElectionIdAndStatus(electionId, NominationStatus.APPROVED); + long pending = nominationRepository.countByElectionIdAndStatusIn(electionId, + List.of(NominationStatus.SUBMITTED, NominationStatus.UNDER_REVIEW, NominationStatus.APPROVAL_PENDING)); + long rejected = nominationRepository.countByElectionIdAndStatus(electionId, NominationStatus.REJECTED); + + return PagedResponse.builder() + .content(result.getContent().stream().map(this::toNominationData).collect(Collectors.toList())) + .pageNumber(result.getNumber()) + .pageSize(result.getSize()) + .totalElements(result.getTotalElements()) + .totalPages(result.getTotalPages()) + .first(result.isFirst()) + .last(result.isLast()) + .summaryApproved(approved) + .summaryPending(pending) + .summaryRejected(rejected) + .build(); } @Override @@ -96,6 +140,16 @@ public class ElectionReadPlatformServiceImpl implements ElectionReadPlatformServ .checkedBy(ec.getCheckedBy()) .checkedDate(ec.getCheckedDate()) .remarks(ec.getRemarks()) + .ageEligible(ec.getAgeEligible()) + .genderEligible(ec.getGenderEligible()) + .casteEligible(ec.getCasteEligible()) + .shareHoldingEligible(ec.getShareHoldingEligible()) + .areaEligible(ec.getAreaEligible()) + .ageCheckRemarks(ec.getAgeCheckRemarks()) + .genderCheckRemarks(ec.getGenderCheckRemarks()) + .casteCheckRemarks(ec.getCasteCheckRemarks()) + .shareCheckRemarks(ec.getShareCheckRemarks()) + .areaCheckRemarks(ec.getAreaCheckRemarks()) .build())); ceoPrecheckRepository.findByNominationId(nominationId).ifPresent(cp -> @@ -114,12 +168,25 @@ public class ElectionReadPlatformServiceImpl implements ElectionReadPlatformServ ? cp.getNoDueCheckResult().name() : null) .noDueCertificateNumber(cp.getNoDueCertificateNumber()) .noDueGeneratedDate(cp.getNoDueGeneratedDate()) + .kycCheckResult(cp.getKycCheckResult() != null ? cp.getKycCheckResult().name() : null) + .kycCheckRemarks(cp.getKycCheckRemarks()) + .familyKycCheckResult(cp.getFamilyKycCheckResult() != null ? cp.getFamilyKycCheckResult().name() : null) + .familyKycCheckRemarks(cp.getFamilyKycCheckRemarks()) + .guarantorCheckResult(cp.getGuarantorCheckResult() != null ? cp.getGuarantorCheckResult().name() : null) + .guarantorCheckRemarks(cp.getGuarantorCheckRemarks()) + .amlCheckResult(cp.getAmlCheckResult() != null ? cp.getAmlCheckResult().name() : null) + .amlCheckRemarks(cp.getAmlCheckRemarks()) + .familyLoanCheckResult(cp.getFamilyLoanCheckResult() != null ? cp.getFamilyLoanCheckResult().name() : null) + .familyLoanCheckRemarks(cp.getFamilyLoanCheckRemarks()) .overallResult(cp.getOverallResult() != null ? cp.getOverallResult().name() : null) .checkedBy(cp.getCheckedBy()) .checkedDate(cp.getCheckedDate()) .remarks(cp.getRemarks()) .build())); + candidateRepository.findByNominationId(nominationId) + .ifPresent(c -> data.setCandidateId(c.getId())); + data.setApprovalSteps( approvalStepRepository.findByNominationIdOrderByStepNumberAsc(nominationId).stream() .map(s -> ElectionApprovalStepData.builder() @@ -150,43 +217,47 @@ public class ElectionReadPlatformServiceImpl implements ElectionReadPlatformServ @Override @Transactional(readOnly = true) - public List 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()); + public PagedResponse retrieveCandidatesByElection(Long electionId, int page, int size) { + Pageable pageable = PageRequest.of(page, size, + Sort.by("electionPost.postName").ascending().and(Sort.by("serialNumber").ascending())); + Page result = candidateRepository.findByElectionConfigIdAndIsActiveTrue(electionId, pageable); + return toCandidatePagedResponse(electionId, result); } @Override @Transactional(readOnly = true) - public List 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()); + public PagedResponse retrieveCandidatesByPost(Long electionId, Long postId, int page, int size) { + Pageable pageable = PageRequest.of(page, size, Sort.by("serialNumber").ascending()); + Page result = candidateRepository + .findByElectionConfigIdAndElectionPostIdAndIsActiveTrue(electionId, postId, pageable); + return toCandidatePagedResponse(electionId, result); + } + + private PagedResponse toCandidatePagedResponse(Long electionId, Page result) { + return PagedResponse.builder() + .content(result.getContent().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())) + .pageNumber(result.getNumber()) + .pageSize(result.getSize()) + .totalElements(result.getTotalElements()) + .totalPages(result.getTotalPages()) + .first(result.isFirst()) + .last(result.isLast()) + .build(); } @Override @@ -264,12 +335,8 @@ public class ElectionReadPlatformServiceImpl implements ElectionReadPlatformServ 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"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionWritePlatformService.java index f22c3bfb5..b3a779efe 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionWritePlatformService.java @@ -25,6 +25,8 @@ public interface ElectionWritePlatformService { void withdrawNomination(Long nominationId); + void resubmitNomination(Long nominationId); + void runEligibilityCheck(Long nominationId); void runCeoPreCheck(Long nominationId); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionWritePlatformServiceImpl.java index 513cde0b0..ef6047fef 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/ElectionWritePlatformServiceImpl.java @@ -34,6 +34,7 @@ import org.apache.fineract.portfolio.election.exception.ElectionNotActiveExcepti 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.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -65,6 +66,7 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe private final ElectionEligibilityService eligibilityService; private final ElectionCeoPreCheckService ceoPreCheckService; private final ElectionNoDueCertificateService noDueCertificateService; + private final JdbcTemplate jdbcTemplate; private static final Set ONGOING_STATUSES = Set.of( ElectionStatus.ACTIVE, @@ -178,11 +180,20 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe throw new ElectionNotActiveException(election.getId(), election.getStatus().name()); } + Integer activeShareCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM m_share_account WHERE client_id = ? AND status_enum = 300", + Integer.class, request.getMemberId()); + if (activeShareCount == null || activeShareCount == 0) { + throw new IllegalArgumentException( + "Member does not have an active share account. Only shareholders can apply for nomination."); + } + ElectionPost post = electionPostRepository.findById(request.getPostId()) .orElseThrow(() -> new IllegalArgumentException("Post not found: " + request.getPostId())); - if (nominationRepository.existsByElectionConfigIdAndMemberIdAndElectionPostId( - request.getElectionId(), request.getMemberId(), request.getPostId())) { + if (nominationRepository.existsByElectionConfigIdAndMemberIdAndStatusNotIn( + request.getElectionId(), request.getMemberId(), + List.of(NominationStatus.REJECTED, NominationStatus.WITHDRAWN))) { throw new NominationAlreadyExistsException( request.getMemberId(), request.getElectionId(), request.getPostId()); } @@ -217,6 +228,20 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe nominationRepository.save(nomination); } + @Override + @Transactional + public void resubmitNomination(Long nominationId) { + ElectionNomination nomination = nominationRepository.findById(nominationId) + .orElseThrow(() -> new ElectionNominationNotFoundException(nominationId)); + if (nomination.getStatus() != NominationStatus.REJECTED) { + throw new InvalidNominationStateException("resubmit", nomination.getStatus().name()); + } + ceoPrecheckRepository.findByNominationId(nominationId) + .ifPresent(ceoPrecheckRepository::delete); + nomination.resubmit(); + nominationRepository.save(nomination); + } + @Override @Transactional public void runEligibilityCheck(Long nominationId) { @@ -237,10 +262,13 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe ElectionNomination nomination = nominationRepository.findById(nominationId) .orElseThrow(() -> new ElectionNominationNotFoundException(nominationId)); - nomination.sendToCeoCheck(); - nominationRepository.save(nomination); ceoPreCheckService.performDirectorLoanCheck(nomination); ceoPreCheckService.performRelationAccountCheck(nomination); + ceoPreCheckService.performNomineeKycCheck(nomination); + ceoPreCheckService.performFamilyKycCheck(nomination); + ceoPreCheckService.performGuarantorCheck(nomination); + ceoPreCheckService.performAmlCheck(nomination); + ceoPreCheckService.performFamilyLoanCheck(nomination); ceoPrecheckRepository.findByNominationId(nominationId).ifPresent(precheck -> { precheck.finalise(context.authenticatedUser().getId(), "CEO pre-check completed"); @@ -248,7 +276,29 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe if (org.apache.fineract.portfolio.election.domain.CheckResult.PASS == precheck.getOverallResult()) { nomination.ceoApprove(); } else { - nomination.ceoReject("Pre-check failed: " + precheck.getRemarks()); + StringBuilder reason = new StringBuilder("Pre-check failed"); + if (precheck.getDirectorLoanCheckResult() != org.apache.fineract.portfolio.election.domain.CheckResult.PASS) { + reason.append("; Director Loan: ").append(precheck.getDirectorLoanRemarks()); + } + if (precheck.getRelationAcCheckResult() != org.apache.fineract.portfolio.election.domain.CheckResult.PASS) { + reason.append("; Family Share Account: ").append(precheck.getRelationAcRemarks()); + } + if (precheck.getKycCheckResult() != org.apache.fineract.portfolio.election.domain.CheckResult.PASS) { + reason.append("; KYC: ").append(precheck.getKycCheckRemarks()); + } + if (precheck.getFamilyKycCheckResult() != org.apache.fineract.portfolio.election.domain.CheckResult.PASS) { + reason.append("; Family KYC: ").append(precheck.getFamilyKycCheckRemarks()); + } + if (precheck.getGuarantorCheckResult() != org.apache.fineract.portfolio.election.domain.CheckResult.PASS) { + reason.append("; Guarantor: ").append(precheck.getGuarantorCheckRemarks()); + } + if (precheck.getAmlCheckResult() != org.apache.fineract.portfolio.election.domain.CheckResult.PASS) { + reason.append("; AML: ").append(precheck.getAmlCheckRemarks()); + } + if (precheck.getFamilyLoanCheckResult() != org.apache.fineract.portfolio.election.domain.CheckResult.PASS) { + reason.append("; Family Loan: ").append(precheck.getFamilyLoanCheckRemarks()); + } + nomination.ceoReject(reason.toString()); } nominationRepository.save(nomination); }); @@ -270,13 +320,13 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe ElectionNomination nomination = nominationRepository.findById(nominationId) .orElseThrow(() -> new ElectionNominationNotFoundException(nominationId)); - if (nomination.getStatus() == NominationStatus.CEO_APPROVED + if (nomination.getStatus() == NominationStatus.APPROVAL_PENDING && nomination.getApprovalSteps().isEmpty()) { initApprovalSteps(nomination); } ElectionApprovalStep currentStep = approvalStepRepository - .findNextPendingStep(nominationId) + .findFirstByNominationIdAndStatusOrderByStepNumberAsc(nominationId, ApprovalStepStatus.PENDING) .orElseThrow(() -> new InvalidNominationStateException("approve", "No pending steps")); Long approverId = context.authenticatedUser().getId(); @@ -315,6 +365,12 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe throw new InvalidNominationStateException("approve candidate", nomination.getStatus().name()); } + // idempotent — if already a candidate, return existing + java.util.Optional existing = candidateRepository.findByNominationId(nominationId); + if (existing.isPresent()) { + return existing.get().getId(); + } + Integer maxSerial = candidateRepository.findMaxSerialNumberByElectionAndPost( nomination.getElectionConfig().getId(), nomination.getElectionPost().getId()) .orElse(0); @@ -328,7 +384,7 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe approvedBy != null ? approvedBy : context.authenticatedUser().getId() ); - return candidateRepository.save(candidate).getId(); + return candidateRepository.saveAndFlush(candidate).getId(); } @Override diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 7500d4c10..3fbb671fa 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -365,4 +365,6 @@ + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0338_election_ceo_precheck_extended_checks.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0338_election_ceo_precheck_extended_checks.xml new file mode 100644 index 000000000..d824a5299 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0338_election_ceo_precheck_extended_checks.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + status = 'DRAFT' + + + + + status = 'CEO_CHECK_PENDING' + + + + + status = 'CEO_APPROVED' + + + + + status = 'CEO_REJECTED' + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0339_election_ceo_precheck_family_loan_check.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0339_election_ceo_precheck_family_loan_check.xml new file mode 100644 index 000000000..44c6127be --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0339_election_ceo_precheck_family_loan_check.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + +