feat(election): server-side pagination, family loan CEO pre-check, extended checks
- PagedResponse<T>: generic paginated wrapper with statusCounts and summary fields - ElectionConfigRepository: pageable query + countGroupByStatus - ElectionNominationRepository: pageable queries + count-by-status helpers - ElectionCandidateRepository: pageable queries by election and post - ElectionReadPlatformService: all list methods return PagedResponse with page/size params - ElectionReadPlatformServiceImpl: add missing Pageable import; paginated impls with summary data - API resources: @QueryParam page/size with @DefaultValue on all 3 list endpoints - ElectionCeoPrecheck: add familyLoanCheckResult + familyLoanCheckRemarks (7th auto-check) - ElectionCeoPreCheckServiceImpl: fix directorLoan check (overdue only); add family loan check covering manual members (unverifiable→FAIL) and linked CBS clients (overdue→FAIL) - Liquibase 0338/0339: extended CEO pre-check columns + family loan check columns - changelog-tenant.xml: include both new changesets Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
fc80869d4b
commit
ed139d6e68
+4
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-4
@@ -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
|
||||
|
||||
+5
-3
@@ -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
|
||||
|
||||
+14
-3
@@ -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)")
|
||||
|
||||
+10
@@ -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;
|
||||
|
||||
+10
@@ -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;
|
||||
}
|
||||
|
||||
+1
@@ -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<ElectionApprovalStepData> approvalSteps;
|
||||
|
||||
+34
@@ -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<T> {
|
||||
|
||||
private List<T> 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<String, Long> statusCounts;
|
||||
|
||||
/** nominations summary across entire filter (not just current page) */
|
||||
private Long summaryApproved;
|
||||
private Long summaryPending;
|
||||
private Long summaryRejected;
|
||||
}
|
||||
+2
-5
@@ -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<ElectionAp
|
||||
|
||||
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);
|
||||
Optional<ElectionApprovalStep> findFirstByNominationIdAndStatusOrderByStepNumberAsc(
|
||||
Long nominationId, ApprovalStepStatus status);
|
||||
|
||||
boolean existsByNominationIdAndStatus(Long nominationId, ApprovalStepStatus status);
|
||||
}
|
||||
|
||||
+7
@@ -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<ElectionCandi
|
||||
|
||||
List<ElectionCandidate> findByElectionConfigIdAndIsActiveTrue(Long electionId);
|
||||
|
||||
Page<ElectionCandidate> findByElectionConfigIdAndIsActiveTrue(Long electionId, Pageable pageable);
|
||||
|
||||
List<ElectionCandidate> findByElectionConfigIdAndElectionPostIdAndIsActiveTrue(
|
||||
Long electionId, Long postId);
|
||||
|
||||
Page<ElectionCandidate> findByElectionConfigIdAndElectionPostIdAndIsActiveTrue(
|
||||
Long electionId, Long postId, Pageable pageable);
|
||||
|
||||
Optional<ElectionCandidate> findByNominationId(Long nominationId);
|
||||
|
||||
@Query("SELECT c FROM ElectionCandidate c WHERE c.electionConfig.id = :electionId " +
|
||||
|
||||
+73
-3
@@ -50,6 +50,41 @@ public class ElectionCeoPrecheck extends AbstractPersistableCustom<Long> {
|
||||
@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<Long> {
|
||||
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<Long> {
|
||||
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<Long> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -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<ElectionConfig,
|
||||
|
||||
List<ElectionConfig> findByIsActiveTrue();
|
||||
|
||||
Page<ElectionConfig> findByIsActiveTrue(Pageable pageable);
|
||||
|
||||
@Query("SELECT e.status, COUNT(e) FROM ElectionConfig e WHERE e.isActive = true GROUP BY e.status")
|
||||
List<Object[]> countGroupByStatus();
|
||||
|
||||
List<ElectionConfig> findByStatus(ElectionStatus status);
|
||||
|
||||
List<ElectionConfig> findByElectionType(ElectionType electionType);
|
||||
|
||||
+40
@@ -32,6 +32,36 @@ public class ElectionEligibilityCheck extends AbstractPersistableCustom<Long> {
|
||||
@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<Long> {
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
+9
-8
@@ -74,7 +74,7 @@ public class ElectionNomination extends AbstractAuditableWithUTCDateTimeCustom<L
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 50)
|
||||
private NominationStatus status = NominationStatus.DRAFT;
|
||||
private NominationStatus status = NominationStatus.SUBMITTED;
|
||||
|
||||
@Column(name = "rejection_reason", columnDefinition = "TEXT")
|
||||
private String rejectionReason;
|
||||
@@ -114,7 +114,7 @@ public class ElectionNomination extends AbstractAuditableWithUTCDateTimeCustom<L
|
||||
nomination.address = address;
|
||||
nomination.occupation = occupation;
|
||||
nomination.shareCount = shareCount;
|
||||
nomination.status = NominationStatus.DRAFT;
|
||||
nomination.status = NominationStatus.SUBMITTED;
|
||||
return nomination;
|
||||
}
|
||||
|
||||
@@ -128,16 +128,12 @@ public class ElectionNomination extends AbstractAuditableWithUTCDateTimeCustom<L
|
||||
this.status = NominationStatus.UNDER_REVIEW;
|
||||
}
|
||||
|
||||
public void sendToCeoCheck() {
|
||||
this.status = NominationStatus.CEO_CHECK_PENDING;
|
||||
}
|
||||
|
||||
public void ceoApprove() {
|
||||
this.status = NominationStatus.CEO_APPROVED;
|
||||
this.status = NominationStatus.APPROVAL_PENDING;
|
||||
}
|
||||
|
||||
public void ceoReject(String reason) {
|
||||
this.status = NominationStatus.CEO_REJECTED;
|
||||
this.status = NominationStatus.REJECTED;
|
||||
this.rejectionReason = reason;
|
||||
}
|
||||
|
||||
@@ -157,4 +153,9 @@ public class ElectionNomination extends AbstractAuditableWithUTCDateTimeCustom<L
|
||||
public void withdraw() {
|
||||
this.status = NominationStatus.WITHDRAWN;
|
||||
}
|
||||
|
||||
public void resubmit() {
|
||||
this.status = NominationStatus.UNDER_REVIEW;
|
||||
this.rejectionReason = null;
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -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,16 +15,29 @@ public interface ElectionNominationRepository extends JpaRepository<ElectionNomi
|
||||
|
||||
List<ElectionNomination> findByElectionConfigId(Long electionId);
|
||||
|
||||
Page<ElectionNomination> findByElectionConfigId(Long electionId, Pageable pageable);
|
||||
|
||||
List<ElectionNomination> findByElectionConfigIdAndStatus(Long electionId, NominationStatus status);
|
||||
|
||||
Page<ElectionNomination> findByElectionConfigIdAndStatus(Long electionId, NominationStatus status, Pageable pageable);
|
||||
|
||||
List<ElectionNomination> 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<NominationStatus> statuses);
|
||||
|
||||
Optional<ElectionNomination> findByElectionConfigIdAndMemberIdAndElectionPostId(
|
||||
Long electionId, Long memberId, Long postId);
|
||||
|
||||
boolean existsByElectionConfigIdAndMemberIdAndElectionPostId(
|
||||
Long electionId, Long memberId, Long postId);
|
||||
|
||||
boolean existsByElectionConfigIdAndMemberIdAndStatusNotIn(
|
||||
Long electionId, Long memberId, java.util.Collection<NominationStatus> statuses);
|
||||
|
||||
Optional<ElectionNomination> findByApplicationNumber(String applicationNumber);
|
||||
|
||||
@Query("SELECT n FROM ElectionNomination n WHERE n.electionConfig.id = :electionId " +
|
||||
|
||||
-4
@@ -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,
|
||||
|
||||
+10
@@ -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);
|
||||
}
|
||||
|
||||
+279
-41
@@ -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<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||
query, nomination.getShareAccountId());
|
||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList(query, nomination.getMemberId());
|
||||
|
||||
List<String> relatedAccounts = new ArrayList<>();
|
||||
for (Map<String, Object> 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<String, Object> 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<Map<String, Object>> rows = jdbcTemplate.queryForList(query, nomination.getMemberId());
|
||||
|
||||
if (rows.isEmpty()) {
|
||||
precheck.completeFamilyKycCheck(CheckResult.PASS, "No linked family members found");
|
||||
} else {
|
||||
List<String> issues = new ArrayList<>();
|
||||
for (Map<String, Object> 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<Map<String, Object>> rows = jdbcTemplate.queryForList(query, nomination.getMemberId());
|
||||
|
||||
if (rows.isEmpty()) {
|
||||
precheck.completeGuarantorCheck(CheckResult.PASS, "No overdue guarantor obligations found");
|
||||
} else {
|
||||
List<String> overdueList = new ArrayList<>();
|
||||
for (Map<String, Object> 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<Map<String, Object>> rows = jdbcTemplate.queryForList(query, nomination.getMemberId());
|
||||
|
||||
if (rows.isEmpty()) {
|
||||
precheck.completeAmlCheck(CheckResult.PASS, "No AML flags found");
|
||||
} else {
|
||||
Map<String, Object> rec = rows.get(0);
|
||||
List<String> 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<String> 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<Map<String, Object>> manualRows = jdbcTemplate.queryForList(manualQuery, nomination.getMemberId());
|
||||
for (Map<String, Object> 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<Map<String, Object>> overdueRows = jdbcTemplate.queryForList(linkedQuery, nomination.getMemberId());
|
||||
for (Map<String, Object> 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) {
|
||||
|
||||
+2
-4
@@ -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());
|
||||
|
||||
+5
-4
@@ -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<ElectionConfigData> retrieveAllElections();
|
||||
PagedResponse<ElectionConfigData> retrieveAllElections(int page, int size);
|
||||
|
||||
ElectionConfigData retrieveElection(Long electionId);
|
||||
|
||||
List<ElectionNominationData> retrieveNominationsByElection(Long electionId, String status);
|
||||
PagedResponse<ElectionNominationData> retrieveNominationsByElection(Long electionId, String status, int page, int size);
|
||||
|
||||
ElectionNominationData retrieveNomination(Long nominationId);
|
||||
|
||||
List<ElectionNominationData> retrieveNominationsByMember(Long memberId);
|
||||
|
||||
List<ElectionCandidateData> retrieveCandidatesByElection(Long electionId);
|
||||
PagedResponse<ElectionCandidateData> retrieveCandidatesByElection(Long electionId, int page, int size);
|
||||
|
||||
List<ElectionCandidateData> retrieveCandidatesByPost(Long electionId, Long postId);
|
||||
PagedResponse<ElectionCandidateData> retrieveCandidatesByPost(Long electionId, Long postId, int page, int size);
|
||||
|
||||
List<ElectionResultData> retrieveResultsByElection(Long electionId);
|
||||
}
|
||||
|
||||
+114
-47
@@ -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<ElectionConfigData> retrieveAllElections() {
|
||||
return electionConfigRepository.findByIsActiveTrue().stream()
|
||||
.map(this::toConfigData)
|
||||
.collect(Collectors.toList());
|
||||
public PagedResponse<ElectionConfigData> retrieveAllElections(int page, int size) {
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "electionDate"));
|
||||
Page<ElectionConfig> result = electionConfigRepository.findByIsActiveTrue(pageable);
|
||||
|
||||
Map<String, Long> statusCounts = new HashMap<>();
|
||||
electionConfigRepository.countGroupByStatus()
|
||||
.forEach(row -> statusCounts.put(row[0].toString(), (Long) row[1]));
|
||||
|
||||
return PagedResponse.<ElectionConfigData>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<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());
|
||||
public PagedResponse<ElectionNominationData> retrieveNominationsByElection(Long electionId, String status, int page, int size) {
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "submittedDate"));
|
||||
Page<ElectionNomination> 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.<ElectionNominationData>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<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());
|
||||
public PagedResponse<ElectionCandidateData> retrieveCandidatesByElection(Long electionId, int page, int size) {
|
||||
Pageable pageable = PageRequest.of(page, size,
|
||||
Sort.by("electionPost.postName").ascending().and(Sort.by("serialNumber").ascending()));
|
||||
Page<ElectionCandidate> result = candidateRepository.findByElectionConfigIdAndIsActiveTrue(electionId, pageable);
|
||||
return toCandidatePagedResponse(electionId, result);
|
||||
}
|
||||
|
||||
@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());
|
||||
public PagedResponse<ElectionCandidateData> retrieveCandidatesByPost(Long electionId, Long postId, int page, int size) {
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by("serialNumber").ascending());
|
||||
Page<ElectionCandidate> result = candidateRepository
|
||||
.findByElectionConfigIdAndElectionPostIdAndIsActiveTrue(electionId, postId, pageable);
|
||||
return toCandidatePagedResponse(electionId, result);
|
||||
}
|
||||
|
||||
private PagedResponse<ElectionCandidateData> toCandidatePagedResponse(Long electionId, Page<ElectionCandidate> result) {
|
||||
return PagedResponse.<ElectionCandidateData>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";
|
||||
|
||||
+2
@@ -25,6 +25,8 @@ public interface ElectionWritePlatformService {
|
||||
|
||||
void withdrawNomination(Long nominationId);
|
||||
|
||||
void resubmitNomination(Long nominationId);
|
||||
|
||||
void runEligibilityCheck(Long nominationId);
|
||||
|
||||
void runCeoPreCheck(Long nominationId);
|
||||
|
||||
+64
-8
@@ -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<ElectionStatus> 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<ElectionCandidate> 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
|
||||
|
||||
@@ -365,4 +365,6 @@
|
||||
<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"/>
|
||||
<include file="parts/0338_election_ceo_precheck_extended_checks.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0339_election_ceo_precheck_family_loan_check.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<changeSet id="0338_add_kyc_check_to_ceo_precheck" author="CBS">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><columnExists tableName="m_election_ceo_precheck" columnName="kyc_check_result"/></not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_election_ceo_precheck">
|
||||
<column name="kyc_check_result" type="VARCHAR(20)" defaultValue="PENDING">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="kyc_check_remarks" type="TEXT"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0338_add_family_kyc_check_to_ceo_precheck" author="CBS">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><columnExists tableName="m_election_ceo_precheck" columnName="family_kyc_check_result"/></not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_election_ceo_precheck">
|
||||
<column name="family_kyc_check_result" type="VARCHAR(20)" defaultValue="PENDING">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="family_kyc_check_remarks" type="TEXT"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0338_add_guarantor_check_to_ceo_precheck" author="CBS">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><columnExists tableName="m_election_ceo_precheck" columnName="guarantor_check_result"/></not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_election_ceo_precheck">
|
||||
<column name="guarantor_check_result" type="VARCHAR(20)" defaultValue="PENDING">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="guarantor_check_remarks" type="TEXT"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0338_add_aml_check_to_ceo_precheck" author="CBS">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><columnExists tableName="m_election_ceo_precheck" columnName="aml_check_result"/></not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_election_ceo_precheck">
|
||||
<column name="aml_check_result" type="VARCHAR(20)" defaultValue="PENDING">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="aml_check_remarks" type="TEXT"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
|
||||
<!-- Migrate existing nomination records with removed statuses -->
|
||||
<changeSet id="0338_migrate_nomination_statuses" author="CBS">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<tableExists tableName="m_election_nomination"/>
|
||||
</preConditions>
|
||||
<!-- DRAFT → SUBMITTED (never submitted, treat as submitted) -->
|
||||
<update tableName="m_election_nomination">
|
||||
<column name="status" value="SUBMITTED"/>
|
||||
<where>status = 'DRAFT'</where>
|
||||
</update>
|
||||
<!-- CEO_CHECK_PENDING → UNDER_REVIEW (was in check phase, re-run check) -->
|
||||
<update tableName="m_election_nomination">
|
||||
<column name="status" value="UNDER_REVIEW"/>
|
||||
<where>status = 'CEO_CHECK_PENDING'</where>
|
||||
</update>
|
||||
<!-- CEO_APPROVED → APPROVAL_PENDING (checks passed, board approval pending) -->
|
||||
<update tableName="m_election_nomination">
|
||||
<column name="status" value="APPROVAL_PENDING"/>
|
||||
<where>status = 'CEO_APPROVED'</where>
|
||||
</update>
|
||||
<!-- CEO_REJECTED → REJECTED (failed checks) -->
|
||||
<update tableName="m_election_nomination">
|
||||
<column name="status" value="REJECTED"/>
|
||||
<where>status = 'CEO_REJECTED'</where>
|
||||
</update>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<changeSet id="0339_add_family_loan_check_to_ceo_precheck" author="CBS">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><columnExists tableName="m_election_ceo_precheck" columnName="family_loan_check_result"/></not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_election_ceo_precheck">
|
||||
<column name="family_loan_check_result" type="VARCHAR(20)" defaultValue="PENDING">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="family_loan_check_remarks" type="TEXT"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user