feat(election): comprehensive audit log fixes — committee logging, deduplicate service logs, fix director entity type

- Add audit logging for all 5 committee operations (create, update, dissolve, add/remove member)
- Add CREATE_COMMITTEE, UPDATE_COMMITTEE, DISSOLVE_COMMITTEE, ADD_COMMITTEE_MEMBER, REMOVE_COMMITTEE_MEMBER to ElectionAuditAction enum
- Fix double logging: remove all duplicate audit calls from ElectionWritePlatformServiceImpl (API resource logs are authoritative)
- Fix double logging in DirectorAppointmentServiceImpl (keep only autoExpireByTenure scheduler log)
- Fix entity type bug in DirectorAppointmentApiResource: "DIRECTOR" -> "DIRECTOR_APPOINTMENT" to match frontend filter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-07-03 17:24:03 +05:30
co-authored by Claude Sonnet 4.6
parent 2606427203
commit a1a31aff25
5 changed files with 53 additions and 116 deletions
@@ -61,12 +61,12 @@ public class DirectorAppointmentApiResource {
id = service.appoint(request);
} catch (RuntimeException e) {
auditService.logInNewTransaction(ElectionAuditAction.APPOINT_DIRECTOR_FAILED,
"DIRECTOR", null, request.getElectionId(),
"DIRECTOR_APPOINTMENT", null, request.getElectionId(),
"Director appointment failed: " + e.getMessage(), null, clientIp(httpRequest));
throw e;
}
auditService.log(ElectionAuditAction.APPOINT_DIRECTOR,
"DIRECTOR", id, request.getElectionId(),
"DIRECTOR_APPOINTMENT", id, request.getElectionId(),
"Director appointed: " + request.getCandidateName() + " as " + request.getDesignation(),
"candidateId=" + request.getCandidateId() + ", post=" + request.getPostName(),
clientIp(httpRequest));
@@ -82,12 +82,12 @@ public class DirectorAppointmentApiResource {
service.update(id, request);
} catch (RuntimeException e) {
auditService.logInNewTransaction(ElectionAuditAction.UPDATE_DIRECTOR_APPOINTMENT_FAILED,
"DIRECTOR", id, request.getElectionId(),
"DIRECTOR_APPOINTMENT", id, request.getElectionId(),
"Director update failed: " + e.getMessage(), null, clientIp(httpRequest));
throw e;
}
auditService.log(ElectionAuditAction.UPDATE_DIRECTOR_APPOINTMENT,
"DIRECTOR", id, request.getElectionId(),
"DIRECTOR_APPOINTMENT", id, request.getElectionId(),
"Director appointment updated: " + request.getCandidateName(),
"designation=" + request.getDesignation() + ", status=" + request.getStatus(),
clientIp(httpRequest));
@@ -103,12 +103,12 @@ public class DirectorAppointmentApiResource {
service.delete(id);
} catch (RuntimeException e) {
auditService.logInNewTransaction(ElectionAuditAction.REMOVE_DIRECTOR_APPOINTMENT_FAILED,
"DIRECTOR", id, null,
"DIRECTOR_APPOINTMENT", id, null,
"Director removal failed: " + e.getMessage(), null, clientIp(httpRequest));
throw e;
}
auditService.log(ElectionAuditAction.REMOVE_DIRECTOR_APPOINTMENT,
"DIRECTOR", id, null,
"DIRECTOR_APPOINTMENT", id, null,
"Director appointment removed (id=" + id + ")", null,
clientIp(httpRequest));
return Response.ok(Map.of("resourceId", id)).build();
@@ -25,5 +25,8 @@ public enum ElectionAuditAction {
DECLARE_RESULTS, DECLARE_RESULTS_FAILED,
// Board of Directors
APPOINT_DIRECTOR, UPDATE_DIRECTOR_APPOINTMENT, REMOVE_DIRECTOR_APPOINTMENT,
APPOINT_DIRECTOR_FAILED, UPDATE_DIRECTOR_APPOINTMENT_FAILED, REMOVE_DIRECTOR_APPOINTMENT_FAILED
APPOINT_DIRECTOR_FAILED, UPDATE_DIRECTOR_APPOINTMENT_FAILED, REMOVE_DIRECTOR_APPOINTMENT_FAILED,
// Committee
CREATE_COMMITTEE, UPDATE_COMMITTEE, DISSOLVE_COMMITTEE,
ADD_COMMITTEE_MEMBER, REMOVE_COMMITTEE_MEMBER
}
@@ -13,6 +13,7 @@ import org.apache.fineract.portfolio.election.domain.CommitteeStatus;
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.ElectionStatus;
@@ -35,6 +36,7 @@ public class CommitteeServiceImpl implements CommitteeService {
private final DirectorAppointmentRepository directorRepository;
private final ElectionCandidateRepository candidateRepository;
private final ElectionConfigRepository electionConfigRepository;
private final ElectionAuditService auditService;
@Override
@Transactional
@@ -51,6 +53,10 @@ public class CommitteeServiceImpl implements CommitteeService {
}
committeeRepository.save(committee);
}
Long electionId = electionIdFromDirectors(request.getDirectorAppointmentIds());
auditService.log(ElectionAuditAction.CREATE_COMMITTEE, "COMMITTEE", committee.getId(), electionId,
"Committee \"" + committee.getName() + "\" created with " + committee.getMembers().size() + " member(s)",
"name=" + committee.getName() + ", members=" + committee.getMembers().size(), null);
return toData(committee);
}
@@ -61,15 +67,23 @@ public class CommitteeServiceImpl implements CommitteeService {
if (request.getName() != null) committee.setName(request.getName());
if (request.getDescription() != null) committee.setDescription(request.getDescription());
committee.setImage(request.getImage());
return toData(committeeRepository.save(committee));
CommitteeData result = toData(committeeRepository.save(committee));
auditService.log(ElectionAuditAction.UPDATE_COMMITTEE, "COMMITTEE", id,
electionIdForCommittee(committee),
"Committee \"" + committee.getName() + "\" updated",
"name=" + committee.getName() + ", description=" + committee.getDescription(), null);
return result;
}
@Override
@Transactional
public void dissolveCommittee(Long id) {
Committee committee = findOrThrow(id);
Long electionId = electionIdForCommittee(committee);
committee.setStatus(CommitteeStatus.DISSOLVED);
committeeRepository.save(committee);
auditService.log(ElectionAuditAction.DISSOLVE_COMMITTEE, "COMMITTEE", id, electionId,
"Committee \"" + committee.getName() + "\" dissolved", "committeeId=" + id, null);
}
@Override
@@ -92,15 +106,25 @@ public class CommitteeServiceImpl implements CommitteeService {
committee.getMembers().add(CommitteeMember.create(committee, dirId));
}
}
return toData(committeeRepository.save(committee));
CommitteeData result = toData(committeeRepository.save(committee));
auditService.log(ElectionAuditAction.ADD_COMMITTEE_MEMBER, "COMMITTEE", committeeId,
electionIdFromDirectors(directorAppointmentIds),
directorAppointmentIds.size() + " member(s) added to committee \"" + committee.getName() + "\"",
"committeeId=" + committeeId + ", addedIds=" + directorAppointmentIds, null);
return result;
}
@Override
@Transactional
public CommitteeData removeMember(Long committeeId, Long directorAppointmentId) {
findOrThrow(committeeId);
Committee committee = findOrThrow(committeeId);
Long electionId = electionIdFromDirectors(List.of(directorAppointmentId));
memberRepository.deleteByCommitteeIdAndDirectorAppointmentId(committeeId, directorAppointmentId);
return toData(findOrThrow(committeeId));
CommitteeData result = toData(findOrThrow(committeeId));
auditService.log(ElectionAuditAction.REMOVE_COMMITTEE_MEMBER, "COMMITTEE", committeeId, electionId,
"Member (appointmentId=" + directorAppointmentId + ") removed from committee \"" + committee.getName() + "\"",
"committeeId=" + committeeId + ", removedAppointmentId=" + directorAppointmentId, null);
return result;
}
@Override
@@ -137,6 +161,19 @@ public class CommitteeServiceImpl implements CommitteeService {
.build()).toList();
}
private Long electionIdFromDirectors(List<Long> directorAppointmentIds) {
if (directorAppointmentIds == null || directorAppointmentIds.isEmpty()) return null;
return directorRepository.findById(directorAppointmentIds.get(0))
.map(DirectorAppointment::getElectionId).orElse(null);
}
private Long electionIdForCommittee(Committee committee) {
return committee.getMembers().stream()
.map(m -> electionIdFromDirectors(List.of(m.getDirectorAppointmentId())))
.filter(id -> id != null)
.findFirst().orElse(null);
}
private Committee findOrThrow(Long id) {
return committeeRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Committee not found: " + id));
@@ -47,10 +47,6 @@ public class DirectorAppointmentServiceImpl implements DirectorAppointmentServic
overrideTenureEndFromElection(req);
validateTenureDates(req);
if (repository.existsByElectionIdAndCandidateId(req.getElectionId(), req.getCandidateId())) {
auditService.log(ElectionAuditAction.APPOINT_DIRECTOR_FAILED, "DIRECTOR_APPOINTMENT", null,
req.getElectionId(),
"Director appointment failed — candidate already appointed",
"candidateId=" + req.getCandidateId() + ", electionId=" + req.getElectionId(), null);
throw new IllegalStateException(
"Candidate " + req.getCandidateId() + " is already appointed for election " + req.getElectionId());
}
@@ -58,15 +54,7 @@ public class DirectorAppointmentServiceImpl implements DirectorAppointmentServic
req.getElectionId(), req.getCandidateId(),
req.getCandidateName(), req.getPostName());
applyFields(d, req);
Long id = repository.saveAndFlush(d).getId();
auditService.log(ElectionAuditAction.APPOINT_DIRECTOR, "DIRECTOR_APPOINTMENT", id,
req.getElectionId(),
req.getCandidateName() + " appointed as " + req.getDesignation(),
"designation=" + req.getDesignation()
+ ", tenureStart=" + req.getTenureStartDate()
+ ", tenureEnd=" + req.getTenureEndDate()
+ ", resolutionNo=" + req.getResolutionNo(), null);
return id;
return repository.saveAndFlush(d).getId();
}
@Override
@@ -96,19 +84,7 @@ public class DirectorAppointmentServiceImpl implements DirectorAppointmentServic
boolean isRemoval = "REMOVED".equalsIgnoreCase(req.getStatus())
&& existing.getStatus() != DirectorAppointmentStatus.REMOVED;
applyFields(existing, req);
DirectorAppointmentData result = toData(repository.save(existing));
ElectionAuditAction action = isRemoval
? ElectionAuditAction.REMOVE_DIRECTOR_APPOINTMENT
: ElectionAuditAction.UPDATE_DIRECTOR_APPOINTMENT;
String description = isRemoval
? existing.getCandidateName() + " marked as Removed"
: existing.getCandidateName() + " appointment updated";
auditService.log(action, "DIRECTOR_APPOINTMENT", id, existing.getElectionId(),
description,
"status=" + req.getStatus()
+ ", designation=" + req.getDesignation()
+ ", remarks=" + req.getRemarks(), null);
return result;
return toData(repository.save(existing));
}
@Override
@@ -28,7 +28,6 @@ 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;
@@ -77,7 +76,6 @@ 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<ElectionStatus> ONGOING_STATUSES = Set.of(
ElectionStatus.ACTIVE,
@@ -125,13 +123,6 @@ 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();
}
@@ -149,9 +140,6 @@ 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
@@ -162,8 +150,6 @@ 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
@@ -173,16 +159,11 @@ 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
@@ -192,16 +173,11 @@ 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
@@ -230,8 +206,6 @@ 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
@@ -242,8 +216,6 @@ 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
@@ -289,11 +261,6 @@ 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();
}
@@ -312,9 +279,6 @@ 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
@@ -334,9 +298,6 @@ 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
@@ -350,10 +311,6 @@ 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
@@ -453,11 +410,6 @@ 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
@@ -495,11 +447,6 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe
}
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;
}
@@ -511,10 +458,6 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe
.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()
@@ -528,10 +471,6 @@ 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
@@ -541,10 +480,6 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe
.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()
@@ -554,10 +489,6 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe
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
@@ -567,10 +498,6 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe
.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()
@@ -579,9 +506,6 @@ public class ElectionWritePlatformServiceImpl implements ElectionWritePlatformSe
candidate.setSymbol(base64Symbol);
candidateRepository.save(candidate);
auditService.log(ElectionAuditAction.UPLOAD_SYMBOL, "CANDIDATE", candidateId,
candidate.getElectionConfig().getId(),
"Symbol uploaded for: " + candidate.getCandidateName(), null, null);
}
@Override
@@ -711,9 +635,6 @@ 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) {