feat(election): date validations, overdue-only NDC/loan checks, audit log gaps fixed
- Added ElectionDateViolationException for proper 4xx on date violations - createElection: validates date order (nominationStart <= nominationEnd <= electionDate <= resultDate) - withdrawNomination/resubmitNomination: blocked after nominationEndDate - closeNominations: requires today >= nominationEndDate - openVoting/closeVoting: requires today >= electionDate - declareResults: requires today >= resultDate (or electionDate if no resultDate) - NDC hasNoDues: checks overdue EMIs only (total_overdue_derived), not outstanding balance - Director/guarantor/family loan checks: overdue-based only — pass if no missed EMIs - MemberHasOutstandingDuesException: proper AbstractPlatformDomainRuleException (was IllegalStateException → 500) - Audit gaps fixed: RESUBMIT_NOMINATION_FAILED added to enum, try/catch added for resubmit, createElection, updateElection; saveManualVoteCounts catches all RuntimeException Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
99cc972804
commit
ba199d78e7
+15
-2
@@ -60,7 +60,14 @@ public class ElectionConfigApiResource {
|
||||
@Operation(summary = "Create a new election")
|
||||
public Response createElection(ElectionConfigRequest request,
|
||||
@Context HttpServletRequest httpRequest) {
|
||||
Long id = writeService.createElection(request);
|
||||
Long id;
|
||||
try {
|
||||
id = writeService.createElection(request);
|
||||
} catch (RuntimeException e) {
|
||||
electionAuditService.logInNewTransaction(ElectionAuditAction.CREATE_ELECTION, "ELECTION", null, null,
|
||||
"Election creation failed: " + e.getMessage(), null, clientIp(httpRequest));
|
||||
throw e;
|
||||
}
|
||||
electionAuditService.log(ElectionAuditAction.CREATE_ELECTION, "ELECTION", id, id,
|
||||
"Election created: " + request.getTitle(),
|
||||
request.toString(),
|
||||
@@ -74,7 +81,13 @@ public class ElectionConfigApiResource {
|
||||
public Response updateElection(@PathParam("electionId") Long electionId,
|
||||
ElectionConfigRequest request,
|
||||
@Context HttpServletRequest httpRequest) {
|
||||
writeService.updateElection(electionId, request);
|
||||
try {
|
||||
writeService.updateElection(electionId, request);
|
||||
} catch (RuntimeException e) {
|
||||
electionAuditService.logInNewTransaction(ElectionAuditAction.UPDATE_ELECTION, "ELECTION", electionId, electionId,
|
||||
"Election update failed: " + e.getMessage(), null, clientIp(httpRequest));
|
||||
throw e;
|
||||
}
|
||||
String updateDetails = null;
|
||||
try { updateDetails = objectMapper.writeValueAsString(request); } catch (Exception ignored) {}
|
||||
electionAuditService.log(ElectionAuditAction.UPDATE_ELECTION, "ELECTION", electionId, electionId,
|
||||
|
||||
+16
-2
@@ -108,7 +108,13 @@ public class ElectionNominationApiResource {
|
||||
public Response resubmitNomination(@PathParam("electionId") Long electionId,
|
||||
@PathParam("nominationId") Long nominationId,
|
||||
@Context HttpServletRequest httpRequest) {
|
||||
writeService.resubmitNomination(nominationId);
|
||||
try {
|
||||
writeService.resubmitNomination(nominationId);
|
||||
} catch (RuntimeException e) {
|
||||
electionAuditService.logInNewTransaction(ElectionAuditAction.RESUBMIT_NOMINATION_FAILED,
|
||||
"NOMINATION", nominationId, electionId, "Resubmit failed: " + e.getMessage(), null, clientIp(httpRequest));
|
||||
throw e;
|
||||
}
|
||||
electionAuditService.log(ElectionAuditAction.RESUBMIT_NOMINATION, "NOMINATION", nominationId, electionId,
|
||||
"Nomination resubmitted", null, clientIp(httpRequest));
|
||||
return Response.ok(Map.of("resourceId", nominationId, "status", "UNDER_REVIEW")).build();
|
||||
@@ -213,7 +219,15 @@ public class ElectionNominationApiResource {
|
||||
public Response generateNoDueCertificate(@PathParam("electionId") Long electionId,
|
||||
@PathParam("nominationId") Long nominationId,
|
||||
@Context HttpServletRequest httpRequest) {
|
||||
String certNumber = writeService.generateNoDueCertificate(nominationId);
|
||||
String certNumber;
|
||||
try {
|
||||
certNumber = writeService.generateNoDueCertificate(nominationId);
|
||||
} catch (org.apache.fineract.portfolio.election.exception.MemberHasOutstandingDuesException e) {
|
||||
electionAuditService.logInNewTransaction(ElectionAuditAction.GENERATE_NO_DUE_CERTIFICATE,
|
||||
"NOMINATION", nominationId, electionId,
|
||||
"NDC generation failed — member has outstanding dues", null, clientIp(httpRequest));
|
||||
throw e;
|
||||
}
|
||||
electionAuditService.log(ElectionAuditAction.GENERATE_NO_DUE_CERTIFICATE, "NOMINATION", nominationId, electionId,
|
||||
"No due certificate generated: " + certNumber, null, clientIp(httpRequest));
|
||||
return Response.ok(Map.of("certificateNumber", certNumber)).build();
|
||||
|
||||
+2
-3
@@ -25,7 +25,6 @@ import org.apache.fineract.portfolio.election.data.ElectionResultData;
|
||||
import org.apache.fineract.portfolio.election.data.ManualVoteCountEntry;
|
||||
import org.apache.fineract.portfolio.election.data.ManualVoteCountsRequest;
|
||||
import org.apache.fineract.portfolio.election.domain.ElectionAuditAction;
|
||||
import org.apache.fineract.portfolio.election.exception.VoteCountExceedsShareAccountsException;
|
||||
import org.apache.fineract.portfolio.election.service.ElectionAuditService;
|
||||
import org.apache.fineract.portfolio.election.service.ElectionReadPlatformService;
|
||||
import org.apache.fineract.portfolio.election.service.ElectionWritePlatformService;
|
||||
@@ -58,10 +57,10 @@ public class ElectionVotingApiResource {
|
||||
String voteDetails;
|
||||
try {
|
||||
voteDetails = writeService.saveManualVoteCounts(electionId, counts, reason);
|
||||
} catch (VoteCountExceedsShareAccountsException e) {
|
||||
} catch (RuntimeException e) {
|
||||
electionAuditService.logInNewTransaction(
|
||||
ElectionAuditAction.MANUAL_VOTE_ENTRY_FAILED, "VOTE", electionId, electionId,
|
||||
"Vote count validation failed: " + e.getDefaultUserMessage(),
|
||||
"Vote count entry failed: " + e.getMessage(),
|
||||
null, clientIp(httpRequest));
|
||||
throw e;
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ public enum ElectionAuditAction {
|
||||
ELIGIBILITY_CHECK, CEO_PRECHECK, APPROVAL_ACTION,
|
||||
GENERATE_NO_DUE_CERTIFICATE,
|
||||
// Nomination — Failed
|
||||
SUBMIT_NOMINATION_FAILED, WITHDRAW_NOMINATION_FAILED,
|
||||
SUBMIT_NOMINATION_FAILED, WITHDRAW_NOMINATION_FAILED, RESUBMIT_NOMINATION_FAILED,
|
||||
ELIGIBILITY_CHECK_FAILED, CEO_PRECHECK_FAILED, APPROVAL_ACTION_FAILED,
|
||||
// Candidate
|
||||
APPROVE_CANDIDATE, REMOVE_CANDIDATE, UPLOAD_SYMBOL, UPDATE_CANDIDATE,
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package org.apache.fineract.portfolio.election.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class ElectionDateViolationException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ElectionDateViolationException(String message) {
|
||||
super("error.msg.election.date.violation", message);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package org.apache.fineract.portfolio.election.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class MemberHasOutstandingDuesException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public MemberHasOutstandingDuesException() {
|
||||
super("error.msg.election.member.outstanding.dues",
|
||||
"Member has outstanding dues. No Due Certificate cannot be issued.");
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -6,6 +6,7 @@ import org.apache.fineract.portfolio.election.domain.CheckResult;
|
||||
import org.apache.fineract.portfolio.election.domain.ElectionCeoPrecheck;
|
||||
import org.apache.fineract.portfolio.election.domain.ElectionCeoPrecheckRepository;
|
||||
import org.apache.fineract.portfolio.election.domain.ElectionNomination;
|
||||
import org.apache.fineract.portfolio.election.exception.MemberHasOutstandingDuesException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -366,7 +367,7 @@ public class ElectionCeoPreCheckServiceImpl implements ElectionCeoPreCheckServic
|
||||
precheck.completeDirectorLoanCheck(CheckResult.FAIL, BigDecimal.ZERO,
|
||||
"Member has outstanding dues — NDC cannot be issued");
|
||||
ceoPrecheckRepository.save(precheck);
|
||||
throw new IllegalStateException("Member has outstanding dues. No Due Certificate cannot be issued.");
|
||||
throw new MemberHasOutstandingDuesException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -28,9 +28,10 @@ public class ElectionNoDueCertificateServiceImpl implements ElectionNoDueCertifi
|
||||
String loanDueQuery = """
|
||||
SELECT COUNT(la.id)
|
||||
FROM m_loan la
|
||||
LEFT JOIN m_loan_arrears_aging a ON a.loan_id = la.id
|
||||
WHERE la.client_id = ?
|
||||
AND la.loan_status_id = 300
|
||||
AND la.total_outstanding_derived > 0
|
||||
AND COALESCE(a.total_overdue_derived, 0) > 0
|
||||
""";
|
||||
Integer loanDueCount = jdbcTemplate.queryForObject(loanDueQuery, Integer.class, memberId);
|
||||
return loanDueCount == null || loanDueCount == 0;
|
||||
|
||||
+54
@@ -29,6 +29,7 @@ import org.apache.fineract.portfolio.election.domain.ElectionVote;
|
||||
import org.apache.fineract.portfolio.election.domain.ElectionVoteRepository;
|
||||
import org.apache.fineract.portfolio.election.domain.NominationStatus;
|
||||
import org.apache.fineract.portfolio.election.exception.ElectionAlreadyOngoingException;
|
||||
import org.apache.fineract.portfolio.election.exception.ElectionDateViolationException;
|
||||
import org.apache.fineract.portfolio.election.exception.ElectionNotFoundException;
|
||||
import org.apache.fineract.portfolio.election.exception.ElectionNominationNotFoundException;
|
||||
import org.apache.fineract.portfolio.election.exception.ElectionNotActiveException;
|
||||
@@ -42,6 +43,7 @@ import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -90,6 +92,9 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe
|
||||
throw new ElectionAlreadyOngoingException();
|
||||
}
|
||||
|
||||
validateDateOrder(request.getNominationStartDate(), request.getNominationEndDate(),
|
||||
request.getElectionDate(), request.getResultDate());
|
||||
|
||||
ElectionConfig election = ElectionConfig.create(
|
||||
request.getTitle(),
|
||||
request.getDescription(),
|
||||
@@ -102,6 +107,9 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe
|
||||
ElectionConfig saved = electionConfigRepository.saveAndFlush(election);
|
||||
|
||||
if (request.getPosts() != null) {
|
||||
if (request.getPosts().size() > 1) {
|
||||
throw new IllegalArgumentException("Only one election post is allowed per election.");
|
||||
}
|
||||
for (ElectionPostRequest postReq : request.getPosts()) {
|
||||
ElectionPost post = ElectionPost.create(saved, postReq.getPostName(), postReq.getTotalSeats());
|
||||
electionPostRepository.saveAndFlush(post);
|
||||
@@ -142,6 +150,10 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe
|
||||
context.authenticatedUser().validateHasPermissionTo("UPDATE_ELECTION");
|
||||
ElectionConfig election = electionConfigRepository.findById(electionId)
|
||||
.orElseThrow(() -> new ElectionNotFoundException(electionId));
|
||||
if (LocalDate.now().isBefore(election.getNominationEndDate())) {
|
||||
throw new ElectionDateViolationException(
|
||||
"Cannot close nominations before the nomination end date " + election.getNominationEndDate());
|
||||
}
|
||||
election.closeNominations();
|
||||
electionConfigRepository.save(election);
|
||||
}
|
||||
@@ -152,6 +164,10 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe
|
||||
context.authenticatedUser().validateHasPermissionTo("UPDATE_ELECTION");
|
||||
ElectionConfig election = electionConfigRepository.findById(electionId)
|
||||
.orElseThrow(() -> new ElectionNotFoundException(electionId));
|
||||
if (LocalDate.now().isBefore(election.getElectionDate())) {
|
||||
throw new ElectionDateViolationException(
|
||||
"Cannot open voting before election date " + election.getElectionDate());
|
||||
}
|
||||
election.openVoting();
|
||||
electionConfigRepository.save(election);
|
||||
}
|
||||
@@ -163,6 +179,11 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe
|
||||
ElectionConfig election = electionConfigRepository.findById(electionId)
|
||||
.orElseThrow(() -> new ElectionNotFoundException(electionId));
|
||||
|
||||
if (LocalDate.now().isBefore(election.getElectionDate())) {
|
||||
throw new ElectionDateViolationException(
|
||||
"Cannot close voting before election date " + election.getElectionDate());
|
||||
}
|
||||
|
||||
List<ElectionPost> posts = electionPostRepository.findByElectionConfigIdAndIsActiveTrue(electionId);
|
||||
for (ElectionPost post : posts) {
|
||||
List<ElectionCandidate> candidates = candidateRepository
|
||||
@@ -243,6 +264,11 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe
|
||||
if (nomination.getStatus() == NominationStatus.APPROVED) {
|
||||
throw new InvalidNominationStateException("withdraw", nomination.getStatus().name());
|
||||
}
|
||||
ElectionConfig election = nomination.getElectionConfig();
|
||||
if (LocalDate.now().isAfter(election.getNominationEndDate())) {
|
||||
throw new ElectionDateViolationException(
|
||||
"Nomination withdrawal period has ended. Nominations closed on " + election.getNominationEndDate());
|
||||
}
|
||||
nomination.withdraw();
|
||||
nominationRepository.save(nomination);
|
||||
}
|
||||
@@ -255,6 +281,11 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe
|
||||
if (nomination.getStatus() != NominationStatus.REJECTED) {
|
||||
throw new InvalidNominationStateException("resubmit", nomination.getStatus().name());
|
||||
}
|
||||
ElectionConfig election = nomination.getElectionConfig();
|
||||
if (LocalDate.now().isAfter(election.getNominationEndDate())) {
|
||||
throw new ElectionDateViolationException(
|
||||
"Nomination resubmission period has ended. Nominations closed on " + election.getNominationEndDate());
|
||||
}
|
||||
ceoPrecheckRepository.findByNominationId(nominationId)
|
||||
.ifPresent(ceoPrecheckRepository::delete);
|
||||
nomination.resubmit();
|
||||
@@ -521,6 +552,13 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe
|
||||
ElectionConfig election = electionConfigRepository.findById(electionId)
|
||||
.orElseThrow(() -> new ElectionNotFoundException(electionId));
|
||||
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate effectiveDate = election.getResultDate() != null ? election.getResultDate() : election.getElectionDate();
|
||||
if (today.isBefore(effectiveDate)) {
|
||||
throw new ElectionDateViolationException(
|
||||
"Cannot declare results before " + effectiveDate);
|
||||
}
|
||||
|
||||
List<ElectionPost> posts = electionPostRepository.findByElectionConfigIdAndIsActiveTrue(electionId);
|
||||
for (ElectionPost post : posts) {
|
||||
List<ElectionCandidate> candidates = candidateRepository
|
||||
@@ -580,4 +618,20 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe
|
||||
private String generateApplicationNumber(Long nominationId, Long electionId) {
|
||||
return "ENOMN-" + electionId + "-" + String.format("%06d", nominationId);
|
||||
}
|
||||
|
||||
private void validateDateOrder(LocalDate nominationStart, LocalDate nominationEnd,
|
||||
LocalDate electionDate, LocalDate resultDate) {
|
||||
if (nominationStart != null && nominationEnd != null && nominationStart.isAfter(nominationEnd)) {
|
||||
throw new ElectionDateViolationException(
|
||||
"Nomination start date " + nominationStart + " cannot be after nomination end date " + nominationEnd);
|
||||
}
|
||||
if (nominationEnd != null && electionDate != null && nominationEnd.isAfter(electionDate)) {
|
||||
throw new ElectionDateViolationException(
|
||||
"Nomination end date " + nominationEnd + " cannot be after election date " + electionDate);
|
||||
}
|
||||
if (electionDate != null && resultDate != null && electionDate.isAfter(resultDate)) {
|
||||
throw new ElectionDateViolationException(
|
||||
"Election date " + electionDate + " cannot be after result date " + resultDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user