package services import ( "context" "errors" "io" "abdanhafidz.com/go-boilerplate/models/dto" entity "abdanhafidz.com/go-boilerplate/models/entity" http_error "abdanhafidz.com/go-boilerplate/models/error" "abdanhafidz.com/go-boilerplate/repositories" "github.com/google/uuid" "gorm.io/gorm" ) var () type ProblemSetService interface { CreateProblemSet(ctx context.Context, ps entity.ProblemSet) error GetProblemSet(ctx context.Context, id uuid.UUID) (entity.ProblemSet, error) ListProblemSets(ctx context.Context) ([]entity.ProblemSet, error) ListProblemSetsWithPagination(ctx context.Context, p entity.Pagination) ([]entity.ProblemSet, int64, error) UpdateProblemSet(ctx context.Context, ps entity.ProblemSet) error DeleteProblemSet(ctx context.Context, id uuid.UUID) error InjectProblemSetsFromCSV(ctx context.Context, fileName string, reader io.Reader) (dto.ProblemSetInjectResponse, error) AddQuestion(ctx context.Context, q entity.Questions) error BulkAddQuestions(ctx context.Context, problemSetId uuid.UUID, questions []entity.Questions) ([]entity.Questions, error) UpdateQuestion(ctx context.Context, q entity.Questions) error BulkUpdateQuestions(ctx context.Context, problemSetId uuid.UUID, questions []entity.Questions) ([]entity.Questions, error) DeleteQuestion(ctx context.Context, qID uuid.UUID) error ListQuestions(ctx context.Context, psID uuid.UUID) ([]entity.Questions, error) ListQuestionsWithPagination(ctx context.Context, psID uuid.UUID, p entity.Pagination) ([]entity.Questions, int64, error) ListProblemSetsByExam(ctx context.Context, examId uuid.UUID, p entity.Pagination) ([]entity.ProblemSetExamAssign, int64, error) ListCandidateProblemSetsByExam(ctx context.Context, examId uuid.UUID, p entity.Pagination) ([]entity.ProblemSet, int64, error) AssignProblemSetToExam(ctx context.Context, examId uuid.UUID, problemSetId uuid.UUID) error RemoveAssignedProblemSet(ctx context.Context, assignId uuid.UUID) error RemoveAssignedProblemSetByExam(ctx context.Context, examId uuid.UUID, problemSetId uuid.UUID) error GetQuestionById(ctx context.Context, qID uuid.UUID) (entity.Questions, error) ListQuestionsByExam(ctx context.Context, examId uuid.UUID) ([]entity.Questions, error) } type problemSetService struct { problemSetRepository repositories.ProblemSetRepository questionsRepository repositories.QuestionsRepository problemSetExamAssignRepository repositories.ProblemSetExamAssignRepository problemSetInjectRepository repositories.ProblemSetInjectRepository } func NewProblemSetService( problemSetRepository repositories.ProblemSetRepository, questionsRepository repositories.QuestionsRepository, problemSetExamAssignRepository repositories.ProblemSetExamAssignRepository, problemSetInjectRepository repositories.ProblemSetInjectRepository, ) ProblemSetService { return &problemSetService{ problemSetRepository: problemSetRepository, questionsRepository: questionsRepository, problemSetExamAssignRepository: problemSetExamAssignRepository, problemSetInjectRepository: problemSetInjectRepository, } } // ---------------- Problem Set CRUD ---------------- func (s *problemSetService) CreateProblemSet(ctx context.Context, ps entity.ProblemSet) error { return s.problemSetRepository.Create(ctx, ps) } func (s *problemSetService) GetProblemSet(ctx context.Context, id uuid.UUID) (entity.ProblemSet, error) { ps, err := s.problemSetRepository.Get(ctx, id) if err != nil { return entity.ProblemSet{}, http_error.PROBLEM_SET_NOT_FOUND } return ps, nil } func (s *problemSetService) ListProblemSets(ctx context.Context) ([]entity.ProblemSet, error) { return s.problemSetRepository.List(ctx) } func (s *problemSetService) ListProblemSetsWithPagination(ctx context.Context, p entity.Pagination) ([]entity.ProblemSet, int64, error) { return s.problemSetRepository.ListWithPagination(ctx, p) } func (s *problemSetService) UpdateProblemSet(ctx context.Context, ps entity.ProblemSet) error { _, err := s.problemSetRepository.Get(ctx, ps.Id) if err != nil { return http_error.PROBLEM_SET_NOT_FOUND } return s.problemSetRepository.Update(ctx, ps) } func (s *problemSetService) DeleteProblemSet(ctx context.Context, id uuid.UUID) error { _, err := s.problemSetRepository.Get(ctx, id) if err != nil { return http_error.PROBLEM_SET_NOT_FOUND } return s.problemSetRepository.Delete(ctx, id) } // ---------------- Questions ---------------- func (s *problemSetService) AddQuestion(ctx context.Context, q entity.Questions) error { _, err := s.problemSetRepository.Get(ctx, q.ProblemSetId) if err != nil { return http_error.PROBLEM_SET_NOT_FOUND } normalizeDescriptionQuestion(&q) return s.questionsRepository.Create(ctx, q) } func (s *problemSetService) BulkAddQuestions(ctx context.Context, problemSetId uuid.UUID, questions []entity.Questions) ([]entity.Questions, error) { if len(questions) == 0 { return nil, http_error.BAD_REQUEST_ERROR } _, err := s.problemSetRepository.Get(ctx, problemSetId) if err != nil { return nil, http_error.PROBLEM_SET_NOT_FOUND } ids := make([]uuid.UUID, 0, len(questions)) for i := range questions { questions[i].ProblemSetId = problemSetId normalizeDescriptionQuestion(&questions[i]) ids = append(ids, questions[i].Id) } if err := s.questionsRepository.CreateBulk(ctx, questions); err != nil { return nil, err } created, err := s.questionsRepository.ListByProblemSetAndIDs(ctx, problemSetId, ids) if err != nil { return nil, err } byID := make(map[uuid.UUID]entity.Questions, len(created)) for _, item := range created { byID[item.Id] = item } ordered := make([]entity.Questions, 0, len(questions)) for _, item := range questions { if question, ok := byID[item.Id]; ok { ordered = append(ordered, question) } } return ordered, nil } func (s *problemSetService) UpdateQuestion(ctx context.Context, q entity.Questions) error { _, err := s.questionsRepository.Get(ctx, q.Id) if err != nil { return http_error.QUESTION_NOT_FOUND } normalizeDescriptionQuestion(&q) return s.questionsRepository.Update(ctx, q) } func (s *problemSetService) BulkUpdateQuestions(ctx context.Context, problemSetId uuid.UUID, questions []entity.Questions) ([]entity.Questions, error) { if len(questions) == 0 { return nil, http_error.BAD_REQUEST_ERROR } _, err := s.problemSetRepository.Get(ctx, problemSetId) if err != nil { return nil, http_error.PROBLEM_SET_NOT_FOUND } ids := make([]uuid.UUID, 0, len(questions)) seen := make(map[uuid.UUID]struct{}, len(questions)) for _, question := range questions { if _, exists := seen[question.Id]; exists { return nil, http_error.BAD_REQUEST_ERROR } seen[question.Id] = struct{}{} ids = append(ids, question.Id) } existing, err := s.questionsRepository.ListByProblemSetAndIDs(ctx, problemSetId, ids) if err != nil { return nil, err } if len(existing) != len(ids) { return nil, http_error.QUESTION_NOT_FOUND } for i := range questions { questions[i].ProblemSetId = problemSetId normalizeDescriptionQuestion(&questions[i]) } if err := s.questionsRepository.UpdateBulk(ctx, questions); err != nil { return nil, err } updated, err := s.questionsRepository.ListByProblemSetAndIDs(ctx, problemSetId, ids) if err != nil { return nil, err } byID := make(map[uuid.UUID]entity.Questions, len(updated)) for _, item := range updated { byID[item.Id] = item } ordered := make([]entity.Questions, 0, len(questions)) for _, item := range questions { if question, ok := byID[item.Id]; ok { ordered = append(ordered, question) } } return ordered, nil } func (s *problemSetService) DeleteQuestion(ctx context.Context, qID uuid.UUID) error { _, err := s.questionsRepository.Get(ctx, qID) if err != nil { return http_error.QUESTION_NOT_FOUND } return s.questionsRepository.Delete(ctx, qID) } func (s *problemSetService) ListQuestions(ctx context.Context, psID uuid.UUID) ([]entity.Questions, error) { _, err := s.problemSetRepository.Get(ctx, psID) if err != nil { return nil, http_error.PROBLEM_SET_NOT_FOUND } return s.questionsRepository.ListByProblemSet(ctx, psID) } func (s *problemSetService) ListQuestionsWithPagination(ctx context.Context, psID uuid.UUID, p entity.Pagination) ([]entity.Questions, int64, error) { _, err := s.problemSetRepository.Get(ctx, psID) if err != nil { return nil, 0, http_error.PROBLEM_SET_NOT_FOUND } return s.questionsRepository.ListByProblemSetWithPagination(ctx, psID, p) } // ---------------- Exam ↔ Problem Set (Mapping Table) ---------------- func (s *problemSetService) AssignProblemSetToExam(ctx context.Context, examId uuid.UUID, problemSetId uuid.UUID) error { _, err := s.problemSetRepository.Get(ctx, problemSetId) if err != nil { return http_error.PROBLEM_SET_NOT_FOUND } if _, err := s.problemSetExamAssignRepository.GetByExamAndProblemSet(ctx, examId, problemSetId); err == nil { return http_error.DUPLICATE_DATA } else if !errors.Is(err, gorm.ErrRecordNotFound) { return err } assign := entity.ProblemSetExamAssign{ Id: uuid.New(), ExamId: examId, ProblemSetId: problemSetId, } return s.problemSetExamAssignRepository.Create(ctx, assign) } func (s *problemSetService) RemoveAssignedProblemSet(ctx context.Context, assignId uuid.UUID) error { return s.problemSetExamAssignRepository.Delete(ctx, assignId) } func (s *problemSetService) ListProblemSetsByExam(ctx context.Context, examId uuid.UUID, p entity.Pagination) ([]entity.ProblemSetExamAssign, int64, error) { return s.problemSetExamAssignRepository.ListByExam(ctx, examId, p) } func (s *problemSetService) ListCandidateProblemSetsByExam(ctx context.Context, examId uuid.UUID, p entity.Pagination) ([]entity.ProblemSet, int64, error) { return s.problemSetExamAssignRepository.ListCandidateByExam(ctx, examId, p) } func (s *problemSetService) RemoveAssignedProblemSetByExam(ctx context.Context, examId uuid.UUID, problemSetId uuid.UUID) error { _, err := s.problemSetExamAssignRepository.GetByExamAndProblemSet(ctx, examId, problemSetId) if errors.Is(err, gorm.ErrRecordNotFound) { return http_error.DATA_NOT_FOUND } if err != nil { return err } return s.problemSetExamAssignRepository.DeleteByExamAndProblemSet(ctx, examId, problemSetId) } func (s *problemSetService) GetQuestionById(ctx context.Context, qID uuid.UUID) (entity.Questions, error) { question, err := s.questionsRepository.Get(ctx, qID) if err != nil { return entity.Questions{}, err } return question, err } func (s *problemSetService) ListQuestionsByExam(ctx context.Context, examId uuid.UUID) ([]entity.Questions, error) { assign, err := s.problemSetExamAssignRepository.GetByExam(ctx, examId) if err != nil { return []entity.Questions{}, err } return s.questionsRepository.ListByProblemSet(ctx, assign.ProblemSetId) } func normalizeDescriptionQuestion(q *entity.Questions) { if q == nil || q.Type != "description" { return } q.Options = nil q.AnsKey = nil q.CorrMark = 0 q.IncorrMark = 0 q.NullMark = 0 }