This commit is contained in:
Aditya Siregar
2025-08-15 21:17:19 +07:00
parent de60983e4e
commit 826c5d26ad
65 changed files with 2625 additions and 669 deletions
+5 -4
View File
@@ -58,7 +58,7 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest)
roles, _ := s.userProcessor.GetUserRoles(ctx, userResponse.ID)
permCodes, _ := s.userProcessor.GetUserPermissionCodes(ctx, userResponse.ID)
departments, _ := s.userProcessor.GetUserDepartments(ctx, userResponse.ID)
// Departments are now preloaded, so they're already in userResponse
token, expiresAt, err := s.generateToken(userResponse, roles, permCodes)
if err != nil {
@@ -71,7 +71,7 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest)
User: *userResponse,
Roles: roles,
Permissions: permCodes,
Departments: departments,
Departments: userResponse.DepartmentResponse,
}, nil
}
@@ -90,6 +90,7 @@ func (s *AuthServiceImpl) ValidateToken(tokenString string) (*contract.UserRespo
return nil, fmt.Errorf("user account is deactivated")
}
// Departments are now preloaded, so they're already in the response
return userResponse, nil
}
@@ -115,14 +116,14 @@ func (s *AuthServiceImpl) RefreshToken(ctx context.Context, tokenString string)
return nil, fmt.Errorf("failed to generate token: %w", err)
}
departments, _ := s.userProcessor.GetUserDepartments(ctx, userResponse.ID)
// Departments are now preloaded, so they're already in userResponse
return &contract.LoginResponse{
Token: newToken,
ExpiresAt: expiresAt,
User: *userResponse,
Roles: roles,
Permissions: permCodes,
Departments: departments,
Departments: userResponse.DepartmentResponse,
}, nil
}
+3 -3
View File
@@ -16,7 +16,7 @@ type LetterProcessor interface {
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error)
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
@@ -50,8 +50,8 @@ func (s *LetterServiceImpl) CreateDispositions(ctx context.Context, req *contrac
return s.processor.CreateDispositions(ctx, req)
}
func (s *LetterServiceImpl) ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error) {
return s.processor.ListDispositionsByLetter(ctx, letterID)
func (s *LetterServiceImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) {
return s.processor.GetEnhancedDispositionsByLetter(ctx, letterID)
}
func (s *LetterServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
+3
View File
@@ -26,4 +26,7 @@ type UserProcessor interface {
// New optimized listing
ListUsersWithFilters(ctx context.Context, req *contract.ListUsersRequest) ([]contract.UserResponse, int, error)
// Get active users for mention purposes
GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error)
}
+39
View File
@@ -30,6 +30,40 @@ func (s *UserServiceImpl) CreateUser(ctx context.Context, req *contract.CreateUs
return s.userProcessor.CreateUser(ctx, req)
}
func (s *UserServiceImpl) BulkCreateUsers(ctx context.Context, req *contract.BulkCreateUsersRequest) (*contract.BulkCreateUsersResponse, error) {
response := &contract.BulkCreateUsersResponse{
Created: []contract.UserResponse{},
Failed: []contract.BulkUserErrorResult{},
Summary: contract.BulkCreationSummary{
Total: len(req.Users),
Succeeded: 0,
Failed: 0,
},
}
for _, userReq := range req.Users {
createReq := &contract.CreateUserRequest{
Name: userReq.Name,
Email: userReq.Email,
Password: userReq.Password,
}
userResponse, err := s.userProcessor.CreateUser(ctx, createReq)
if err != nil {
response.Failed = append(response.Failed, contract.BulkUserErrorResult{
User: userReq,
Error: err.Error(),
})
response.Summary.Failed++
} else {
response.Created = append(response.Created, *userResponse)
response.Summary.Succeeded++
}
}
return response, nil
}
func (s *UserServiceImpl) UpdateUser(ctx context.Context, id uuid.UUID, req *contract.UpdateUserRequest) (*contract.UserResponse, error) {
return s.userProcessor.UpdateUser(ctx, id, req)
}
@@ -96,3 +130,8 @@ func (s *UserServiceImpl) ListTitles(ctx context.Context) (*contract.ListTitlesR
}
return &contract.ListTitlesResponse{Titles: transformer.TitlesToContract(titles)}, nil
}
// GetActiveUsersForMention retrieves active users for mention purposes
func (s *UserServiceImpl) GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error) {
return s.userProcessor.GetActiveUsersForMention(ctx, search, limit)
}
+258
View File
@@ -0,0 +1,258 @@
package service
import (
"context"
"errors"
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
"eslogad-be/internal/transformer"
"github.com/google/uuid"
)
type VoteEventRepository interface {
Create(ctx context.Context, voteEvent *entities.VoteEvent) error
GetByID(ctx context.Context, id uuid.UUID) (*entities.VoteEvent, error)
GetActiveEvents(ctx context.Context) ([]*entities.VoteEvent, error)
List(ctx context.Context, limit, offset int) ([]*entities.VoteEvent, int64, error)
Update(ctx context.Context, voteEvent *entities.VoteEvent) error
Delete(ctx context.Context, id uuid.UUID) error
CreateCandidate(ctx context.Context, candidate *entities.Candidate) error
GetCandidatesByEventID(ctx context.Context, eventID uuid.UUID) ([]*entities.Candidate, error)
SubmitVote(ctx context.Context, vote *entities.Vote) error
HasUserVoted(ctx context.Context, userID, eventID uuid.UUID) (bool, error)
GetVoteResults(ctx context.Context, eventID uuid.UUID) (map[uuid.UUID]int64, error)
}
type VoteEventServiceImpl struct {
voteEventRepo VoteEventRepository
}
func NewVoteEventService(voteEventRepo VoteEventRepository) *VoteEventServiceImpl {
return &VoteEventServiceImpl{
voteEventRepo: voteEventRepo,
}
}
func (s *VoteEventServiceImpl) CreateVoteEvent(ctx context.Context, req *contract.CreateVoteEventRequest) (*contract.VoteEventResponse, error) {
if req.EndDate.Before(req.StartDate) {
return nil, errors.New("end date must be after start date")
}
voteEvent := &entities.VoteEvent{
Title: req.Title,
Description: req.Description,
StartDate: req.StartDate,
EndDate: req.EndDate,
IsActive: true,
ResultsOpen: false,
}
if req.ResultsOpen != nil {
voteEvent.ResultsOpen = *req.ResultsOpen
}
if err := s.voteEventRepo.Create(ctx, voteEvent); err != nil {
return nil, err
}
return transformer.VoteEventToContract(voteEvent), nil
}
func (s *VoteEventServiceImpl) GetVoteEventByID(ctx context.Context, id uuid.UUID) (*contract.VoteEventResponse, error) {
voteEvent, err := s.voteEventRepo.GetByID(ctx, id)
if err != nil {
return nil, err
}
return transformer.VoteEventToContract(voteEvent), nil
}
func (s *VoteEventServiceImpl) GetActiveEvents(ctx context.Context) ([]contract.VoteEventResponse, error) {
events, err := s.voteEventRepo.GetActiveEvents(ctx)
if err != nil {
return nil, err
}
var responses []contract.VoteEventResponse
for _, event := range events {
responses = append(responses, *transformer.VoteEventToContract(event))
}
return responses, nil
}
func (s *VoteEventServiceImpl) ListVoteEvents(ctx context.Context, req *contract.ListVoteEventsRequest) (*contract.ListVoteEventsResponse, error) {
page := req.Page
if page <= 0 {
page = 1
}
limit := req.Limit
if limit <= 0 {
limit = 10
}
offset := (page - 1) * limit
events, total, err := s.voteEventRepo.List(ctx, limit, offset)
if err != nil {
return nil, err
}
var responses []contract.VoteEventResponse
for _, event := range events {
responses = append(responses, *transformer.VoteEventToContract(event))
}
return &contract.ListVoteEventsResponse{
VoteEvents: responses,
Pagination: transformer.CreatePaginationResponse(int(total), page, limit),
}, nil
}
func (s *VoteEventServiceImpl) UpdateVoteEvent(ctx context.Context, id uuid.UUID, req *contract.UpdateVoteEventRequest) (*contract.VoteEventResponse, error) {
if req.EndDate.Before(req.StartDate) {
return nil, errors.New("end date must be after start date")
}
voteEvent, err := s.voteEventRepo.GetByID(ctx, id)
if err != nil {
return nil, err
}
voteEvent.Title = req.Title
voteEvent.Description = req.Description
voteEvent.StartDate = req.StartDate
voteEvent.EndDate = req.EndDate
voteEvent.IsActive = req.IsActive
if req.ResultsOpen != nil {
voteEvent.ResultsOpen = *req.ResultsOpen
}
if err := s.voteEventRepo.Update(ctx, voteEvent); err != nil {
return nil, err
}
return transformer.VoteEventToContract(voteEvent), nil
}
func (s *VoteEventServiceImpl) DeleteVoteEvent(ctx context.Context, id uuid.UUID) error {
return s.voteEventRepo.Delete(ctx, id)
}
func (s *VoteEventServiceImpl) CreateCandidate(ctx context.Context, req *contract.CreateCandidateRequest) (*contract.CandidateResponse, error) {
voteEvent, err := s.voteEventRepo.GetByID(ctx, req.VoteEventID)
if err != nil {
return nil, err
}
if !voteEvent.IsActive {
return nil, errors.New("cannot add candidates to inactive vote event")
}
candidate := &entities.Candidate{
VoteEventID: req.VoteEventID,
Name: req.Name,
ImageURL: req.ImageURL,
Description: req.Description,
}
if err := s.voteEventRepo.CreateCandidate(ctx, candidate); err != nil {
return nil, err
}
return transformer.CandidateToContract(candidate), nil
}
func (s *VoteEventServiceImpl) GetCandidates(ctx context.Context, eventID uuid.UUID) ([]contract.CandidateResponse, error) {
candidates, err := s.voteEventRepo.GetCandidatesByEventID(ctx, eventID)
if err != nil {
return nil, err
}
var responses []contract.CandidateResponse
for _, candidate := range candidates {
responses = append(responses, *transformer.CandidateToContract(candidate))
}
return responses, nil
}
func (s *VoteEventServiceImpl) SubmitVote(ctx context.Context, userID uuid.UUID, req *contract.SubmitVoteRequest) (*contract.VoteResponse, error) {
voteEvent, err := s.voteEventRepo.GetByID(ctx, req.VoteEventID)
if err != nil {
return nil, err
}
if !voteEvent.IsVotingOpen() {
return nil, errors.New("voting is not open for this event")
}
hasVoted, err := s.voteEventRepo.HasUserVoted(ctx, userID, req.VoteEventID)
if err != nil {
return nil, err
}
if hasVoted {
return nil, errors.New("user has already voted for this event")
}
vote := &entities.Vote{
VoteEventID: req.VoteEventID,
CandidateID: req.CandidateID,
UserID: userID,
}
if err := s.voteEventRepo.SubmitVote(ctx, vote); err != nil {
return nil, err
}
return transformer.VoteToContract(vote), nil
}
func (s *VoteEventServiceImpl) GetVoteResults(ctx context.Context, eventID uuid.UUID) (*contract.VoteResultsResponse, error) {
candidates, err := s.voteEventRepo.GetCandidatesByEventID(ctx, eventID)
if err != nil {
return nil, err
}
voteResults, err := s.voteEventRepo.GetVoteResults(ctx, eventID)
if err != nil {
return nil, err
}
var candidatesWithVotes []contract.CandidateWithVotesResponse
var totalVotes int64
for _, candidate := range candidates {
voteCount := voteResults[candidate.ID]
totalVotes += voteCount
candidatesWithVotes = append(candidatesWithVotes, contract.CandidateWithVotesResponse{
CandidateResponse: *transformer.CandidateToContract(candidate),
VoteCount: voteCount,
})
}
return &contract.VoteResultsResponse{
VoteEventID: eventID,
Candidates: candidatesWithVotes,
TotalVotes: totalVotes,
}, nil
}
func (s *VoteEventServiceImpl) CheckVoteStatus(ctx context.Context, userID, eventID uuid.UUID) (*contract.CheckVoteStatusResponse, error) {
hasVoted, err := s.voteEventRepo.HasUserVoted(ctx, userID, eventID)
if err != nil {
return nil, err
}
response := &contract.CheckVoteStatusResponse{
HasVoted: hasVoted,
}
return response, nil
}