diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/DirectorAppointmentData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/DirectorAppointmentData.java index baf21f35d..174601704 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/DirectorAppointmentData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/data/DirectorAppointmentData.java @@ -17,6 +17,7 @@ public class DirectorAppointmentData { private Long id; private Long electionId; private Long candidateId; + private Long memberId; private String candidateName; private String postName; private String designation; @@ -28,4 +29,5 @@ public class DirectorAppointmentData { private String remarks; private LocalDateTime createdDate; private LocalDateTime lastModifiedDate; + private String electionTitle; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionConfig.java index 4bf062ba8..34fde4e50 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/domain/ElectionConfig.java @@ -88,6 +88,13 @@ public class ElectionConfig extends AbstractAuditableWithUTCDateTimeCustom return status == ElectionStatus.VOTING_OPEN; } + public boolean isCandidateModificationLocked() { + return status == ElectionStatus.VOTING_OPEN + || status == ElectionStatus.VOTING_CLOSED + || status == ElectionStatus.RESULT_DECLARED + || status == ElectionStatus.CANCELLED; + } + public void activate() { this.status = ElectionStatus.ACTIVE; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/DirectorAppointmentServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/DirectorAppointmentServiceImpl.java index 0d0af12fd..b598fde38 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/DirectorAppointmentServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/election/service/DirectorAppointmentServiceImpl.java @@ -10,6 +10,7 @@ import org.apache.fineract.portfolio.election.domain.DirectorAppointment; import org.apache.fineract.portfolio.election.domain.DirectorAppointmentRepository; import org.apache.fineract.portfolio.election.domain.DirectorAppointmentStatus; import org.apache.fineract.portfolio.election.domain.ElectionAuditAction; +import org.apache.fineract.portfolio.election.domain.ElectionCandidateRepository; import org.apache.fineract.portfolio.election.domain.ElectionConfigRepository; import org.apache.fineract.portfolio.election.domain.ElectionResultRepository; import org.springframework.data.domain.Page; @@ -24,6 +25,9 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -34,6 +38,7 @@ public class DirectorAppointmentServiceImpl implements DirectorAppointmentServic private final DirectorAppointmentRepository repository; private final ElectionResultRepository resultRepository; private final ElectionConfigRepository electionConfigRepository; + private final ElectionCandidateRepository candidateRepository; private final ElectionAuditService auditService; @Override @@ -152,8 +157,25 @@ public class DirectorAppointmentServiceImpl implements DirectorAppointmentServic result = repository.findAll(pageable); } + Set electionIds = result.getContent().stream() + .map(DirectorAppointment::getElectionId) + .collect(Collectors.toSet()); + Map electionTitleMap = electionConfigRepository.findAllById(electionIds) + .stream() + .collect(Collectors.toMap(e -> e.getId(), e -> e.getTitle() != null ? e.getTitle() : "Election #" + e.getId())); + + Set candidateIds = result.getContent().stream() + .map(DirectorAppointment::getCandidateId) + .collect(Collectors.toSet()); + Map candidateMemberMap = candidateRepository.findAllById(candidateIds) + .stream() + .filter(c -> c.getNomination() != null && c.getNomination().getMemberId() != null) + .collect(Collectors.toMap(c -> c.getId(), c -> c.getNomination().getMemberId())); + return PagedResponse.builder() - .content(result.getContent().stream().map(this::toData).toList()) + .content(result.getContent().stream() + .map(d -> toData(d, electionTitleMap.get(d.getElectionId()), candidateMemberMap.get(d.getCandidateId()))) + .toList()) .pageNumber(result.getNumber()) .pageSize(result.getSize()) .totalElements(result.getTotalElements()) @@ -207,10 +229,19 @@ public class DirectorAppointmentServiceImpl implements DirectorAppointmentServic } private DirectorAppointmentData toData(DirectorAppointment d) { + return toData(d, null, null); + } + + private DirectorAppointmentData toData(DirectorAppointment d, String electionTitle) { + return toData(d, electionTitle, null); + } + + private DirectorAppointmentData toData(DirectorAppointment d, String electionTitle, Long memberId) { return DirectorAppointmentData.builder() .id(d.getId()) .electionId(d.getElectionId()) .candidateId(d.getCandidateId()) + .memberId(memberId) .candidateName(d.getCandidateName()) .postName(d.getPostName()) .designation(d.getDesignation()) @@ -222,6 +253,7 @@ public class DirectorAppointmentServiceImpl implements DirectorAppointmentServic .remarks(d.getRemarks()) .createdDate(d.getCreatedDate()) .lastModifiedDate(d.getLastModifiedDate()) + .electionTitle(electionTitle) .build(); } } 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 618ccd76d..589450854 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 @@ -28,6 +28,7 @@ import org.apache.fineract.portfolio.election.domain.ElectionType; import org.apache.fineract.portfolio.election.domain.ElectionVote; import org.apache.fineract.portfolio.election.domain.ElectionVoteRepository; import org.apache.fineract.portfolio.election.domain.NominationStatus; +import org.apache.fineract.portfolio.election.domain.ElectionAuditAction; import org.apache.fineract.portfolio.election.exception.ElectionAlreadyOngoingException; import org.apache.fineract.portfolio.election.exception.ElectionDateViolationException; import org.apache.fineract.portfolio.election.exception.ElectionDurationActiveException; @@ -76,6 +77,7 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe private final ElectionCeoPreCheckService ceoPreCheckService; private final ElectionNoDueCertificateService noDueCertificateService; private final JdbcTemplate jdbcTemplate; + private final ElectionAuditService auditService; private static final Set ONGOING_STATUSES = Set.of( ElectionStatus.ACTIVE, @@ -123,6 +125,13 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe } } + auditService.log(ElectionAuditAction.CREATE_ELECTION, "ELECTION", saved.getId(), saved.getId(), + "Election created: " + saved.getTitle(), + "title=" + saved.getTitle() + ", type=" + saved.getElectionType() + + ", nominationStart=" + request.getNominationStartDate() + + ", electionDate=" + request.getElectionDate() + + ", durationYears=" + request.getDurationYears(), null); + return saved.getId(); } @@ -140,6 +149,9 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe if (request.getResultDate() != null) election.setResultDate(request.getResultDate()); if (request.getDurationYears() != null) election.setDurationYears(request.getDurationYears()); electionConfigRepository.save(election); + auditService.log(ElectionAuditAction.UPDATE_ELECTION, "ELECTION", electionId, electionId, + "Election updated: " + election.getTitle(), + "title=" + election.getTitle() + ", status=" + election.getStatus(), null); } @Override @@ -150,6 +162,8 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe .orElseThrow(() -> new ElectionNotFoundException(electionId)); election.activate(); electionConfigRepository.save(election); + auditService.log(ElectionAuditAction.ACTIVATE_ELECTION, "ELECTION", electionId, electionId, + "Election activated: " + election.getTitle(), "status=ACTIVE", null); } @Override @@ -159,11 +173,16 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe ElectionConfig election = electionConfigRepository.findById(electionId) .orElseThrow(() -> new ElectionNotFoundException(electionId)); if (LocalDate.now().isBefore(election.getNominationEndDate())) { + auditService.logInNewTransaction(ElectionAuditAction.CLOSE_NOMINATIONS_FAILED, "ELECTION", electionId, electionId, + "Close nominations failed — nomination end date not reached", + "nominationEndDate=" + election.getNominationEndDate(), null); throw new ElectionDateViolationException( "Cannot close nominations before the nomination end date " + election.getNominationEndDate()); } election.closeNominations(); electionConfigRepository.save(election); + auditService.log(ElectionAuditAction.CLOSE_NOMINATIONS, "ELECTION", electionId, electionId, + "Nominations closed for: " + election.getTitle(), "status=NOMINATION_CLOSED", null); } @Override @@ -173,11 +192,16 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe ElectionConfig election = electionConfigRepository.findById(electionId) .orElseThrow(() -> new ElectionNotFoundException(electionId)); if (LocalDate.now().isBefore(election.getElectionDate())) { + auditService.logInNewTransaction(ElectionAuditAction.OPEN_VOTING_FAILED, "ELECTION", electionId, electionId, + "Open voting failed — election date not reached", + "electionDate=" + election.getElectionDate(), null); throw new ElectionDateViolationException( "Cannot open voting before election date " + election.getElectionDate()); } election.openVoting(); electionConfigRepository.save(election); + auditService.log(ElectionAuditAction.OPEN_VOTING, "ELECTION", electionId, electionId, + "Voting opened for: " + election.getTitle(), "status=VOTING_OPEN", null); } @Override @@ -206,6 +230,8 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe election.closeVoting(); electionConfigRepository.save(election); + auditService.log(ElectionAuditAction.CLOSE_VOTING, "ELECTION", electionId, electionId, + "Voting closed for: " + election.getTitle(), "status=VOTING_CLOSED", null); } @Override @@ -216,6 +242,8 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe .orElseThrow(() -> new ElectionNotFoundException(electionId)); election.cancel(reason); electionConfigRepository.save(election); + auditService.log(ElectionAuditAction.CANCEL_ELECTION, "ELECTION", electionId, electionId, + "Election cancelled: " + election.getTitle(), "reason=" + reason, null); } @Override @@ -261,6 +289,11 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe saved.submit(appNumber); nominationRepository.saveAndFlush(saved); + auditService.log(ElectionAuditAction.SUBMIT_NOMINATION, "NOMINATION", saved.getId(), election.getId(), + "Nomination submitted by " + request.getApplicantName(), + "applicant=" + request.getApplicantName() + ", post=" + post.getPostName() + + ", appNo=" + appNumber, null); + return saved.getId(); } @@ -279,6 +312,9 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe } nomination.withdraw(); nominationRepository.save(nomination); + auditService.log(ElectionAuditAction.WITHDRAW_NOMINATION, "NOMINATION", nominationId, + nomination.getElectionConfig().getId(), + "Nomination withdrawn by " + nomination.getApplicantName(), null, null); } @Override @@ -298,6 +334,9 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe .ifPresent(ceoPrecheckRepository::delete); nomination.resubmit(); nominationRepository.save(nomination); + auditService.log(ElectionAuditAction.RESUBMIT_NOMINATION, "NOMINATION", nominationId, + nomination.getElectionConfig().getId(), + "Nomination resubmitted by " + nomination.getApplicantName(), null, null); } @Override @@ -311,6 +350,10 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe eligibilityService.performCheck(nomination, nomination.getElectionPost(), checkedBy); nomination.markUnderReview(); nominationRepository.save(nomination); + auditService.log(ElectionAuditAction.ELIGIBILITY_CHECK, "NOMINATION", nominationId, + nomination.getElectionConfig().getId(), + "Eligibility check run for " + nomination.getApplicantName(), + "checkedBy=" + checkedBy, null); } @Override @@ -410,6 +453,11 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe } nominationRepository.save(nomination); + String action = "APPROVE".equalsIgnoreCase(request.getAction()) ? "approved" : "rejected"; + auditService.log(ElectionAuditAction.APPROVAL_ACTION, "NOMINATION", nominationId, + nomination.getElectionConfig().getId(), + "Approval step " + action + " for " + nomination.getApplicantName(), + "action=" + request.getAction() + ", remarks=" + request.getRemarks(), null); } @Override @@ -446,7 +494,13 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe candidate.setPhotoUrl("/clients/" + nomination.getMemberId() + "/images"); } - return candidateRepository.saveAndFlush(candidate).getId(); + Long candidateId = candidateRepository.saveAndFlush(candidate).getId(); + auditService.log(ElectionAuditAction.APPROVE_CANDIDATE, "CANDIDATE", candidateId, + nomination.getElectionConfig().getId(), + "Candidate approved: " + nomination.getApplicantName(), + "post=" + nomination.getElectionPost().getPostName() + + ", serialNo=" + (maxSerial + 1), null); + return candidateId; } @Override @@ -456,6 +510,17 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe ElectionCandidate candidate = candidateRepository.findById(candidateId) .orElseThrow(() -> new IllegalArgumentException("Candidate not found: " + candidateId)); + if (candidate.getElectionConfig().isCandidateModificationLocked()) { + auditService.logInNewTransaction(ElectionAuditAction.REMOVE_CANDIDATE_FAILED, "CANDIDATE", candidateId, + candidate.getElectionConfig().getId(), + "Remove candidate failed — election locked", + "electionStatus=" + candidate.getElectionConfig().getStatus().name(), null); + throw new IllegalStateException( + "Candidate cannot be removed. Election is in " + + candidate.getElectionConfig().getStatus().name() + + " status — modifications are locked once voting has started."); + } + String symbol = candidate.getSymbol(); if (symbol != null && !symbol.startsWith("data:") && symbol.contains("|")) { try { contentStoreService.delete(symbol.split("\\|", 2)[1]); } catch (Exception ignored) {} @@ -463,6 +528,10 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe candidate.deactivate(); candidateRepository.save(candidate); + auditService.log(ElectionAuditAction.REMOVE_CANDIDATE, "CANDIDATE", candidateId, + candidate.getElectionConfig().getId(), + "Candidate removed: " + candidate.getCandidateName(), + "reason=" + reason, null); } @Override @@ -470,9 +539,25 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe public void updateCandidate(Long candidateId, String symbol, String briefProfile) { ElectionCandidate candidate = candidateRepository.findById(candidateId) .orElseThrow(() -> new IllegalArgumentException("Candidate not found: " + candidateId)); + + if (symbol != null && candidate.getElectionConfig().isCandidateModificationLocked()) { + auditService.logInNewTransaction(ElectionAuditAction.REMOVE_CANDIDATE_FAILED, "CANDIDATE", candidateId, + candidate.getElectionConfig().getId(), + "Symbol update failed — election locked", + "electionStatus=" + candidate.getElectionConfig().getStatus().name(), null); + throw new IllegalStateException( + "Candidate symbol cannot be changed. Election is in " + + candidate.getElectionConfig().getStatus().name() + + " status — symbol changes are locked once voting has started."); + } + if (symbol != null) candidate.setSymbol(symbol); if (briefProfile != null) candidate.setBriefProfile(briefProfile); candidateRepository.save(candidate); + auditService.log(ElectionAuditAction.UPLOAD_SYMBOL, "CANDIDATE", candidateId, + candidate.getElectionConfig().getId(), + "Candidate updated: " + candidate.getCandidateName(), + "symbolChanged=" + (symbol != null) + ", profileChanged=" + (briefProfile != null), null); } @Override @@ -480,8 +565,23 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe public void uploadCandidateSymbol(Long candidateId, String base64Symbol) { ElectionCandidate candidate = candidateRepository.findById(candidateId) .orElseThrow(() -> new IllegalArgumentException("Candidate not found: " + candidateId)); + + if (candidate.getElectionConfig().isCandidateModificationLocked()) { + auditService.logInNewTransaction(ElectionAuditAction.REMOVE_CANDIDATE_FAILED, "CANDIDATE", candidateId, + candidate.getElectionConfig().getId(), + "Symbol upload failed — election locked", + "electionStatus=" + candidate.getElectionConfig().getStatus().name(), null); + throw new IllegalStateException( + "Candidate symbol cannot be uploaded. Election is in " + + candidate.getElectionConfig().getStatus().name() + + " status — symbol changes are locked once voting has started."); + } + candidate.setSymbol(base64Symbol); candidateRepository.save(candidate); + auditService.log(ElectionAuditAction.UPLOAD_SYMBOL, "CANDIDATE", candidateId, + candidate.getElectionConfig().getId(), + "Symbol uploaded for: " + candidate.getCandidateName(), null, null); } @Override @@ -611,6 +711,9 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe election.declareResult(); electionConfigRepository.save(election); + auditService.log(ElectionAuditAction.DECLARE_RESULTS, "ELECTION", electionId, electionId, + "Results declared for: " + election.getTitle(), + "declaredBy=" + declaredBy + ", posts=" + posts.size(), null); } private void initApprovalSteps(ElectionNomination nomination) {