package practicecontent import ( "Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/ports" "context" "errors" "github.com/jackc/pgx/v5" ) type Service struct { questions ports.QuestionStore lms ports.LmsPracticeStore examPrep ports.ExamPrepPracticeStore personas ports.LmsPersonaReader } func NewService( questions ports.QuestionStore, lms ports.LmsPracticeStore, examPrep ports.ExamPrepPracticeStore, personas ports.LmsPersonaReader, ) *Service { return &Service{ questions: questions, lms: lms, examPrep: examPrep, personas: personas, } } func (s *Service) validatePersona(ctx context.Context, id int64) error { if id <= 0 { return domain.ErrPersonaNotFound } _, err := s.personas.GetLmsPersonaByID(ctx, id) if errors.Is(err, pgx.ErrNoRows) { return domain.ErrPersonaNotFound } return err } func (s *Service) validateQuestionSet(ctx context.Context, id int64) error { _, err := s.questions.GetQuestionSetByID(ctx, id) if errors.Is(err, pgx.ErrNoRows) { return ErrQuestionSetNotFound } return err } func mergeQuestionSetInput(existing domain.QuestionSet, in *domain.FullUpdateQuestionSetInput) domain.CreateQuestionSetInput { out := domain.CreateQuestionSetInput{ Title: existing.Title, Description: existing.Description, SetType: existing.SetType, OwnerType: existing.OwnerType, OwnerID: existing.OwnerID, BannerImage: existing.BannerImage, Persona: existing.Persona, TimeLimitMinutes: existing.TimeLimitMinutes, PassingScore: existing.PassingScore, ShuffleQuestions: &existing.ShuffleQuestions, Status: &existing.Status, IntroVideoURL: existing.IntroVideoURL, } if in == nil { return out } if in.Title != nil { out.Title = *in.Title } if in.Description != nil { out.Description = in.Description } if in.BannerImage != nil { out.BannerImage = in.BannerImage } if in.Persona != nil { out.Persona = in.Persona } if in.TimeLimitMinutes != nil { out.TimeLimitMinutes = in.TimeLimitMinutes } if in.PassingScore != nil { out.PassingScore = in.PassingScore } if in.ShuffleQuestions != nil { out.ShuffleQuestions = in.ShuffleQuestions } if in.Status != nil { out.Status = in.Status } if in.IntroVideoURL != nil { out.IntroVideoURL = in.IntroVideoURL } return out } func (s *Service) applySharedUpdates( ctx context.Context, questionSetID int64, in domain.FullUpdatePracticeInput, ) (domain.QuestionSet, []domain.QuestionWithDetails, error) { if in.QuestionSet != nil { existingSet, err := s.questions.GetQuestionSetByID(ctx, questionSetID) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return domain.QuestionSet{}, nil, ErrQuestionSetNotFound } return domain.QuestionSet{}, nil, err } merged := mergeQuestionSetInput(existingSet, in.QuestionSet) if err := s.questions.UpdateQuestionSet(ctx, questionSetID, merged); err != nil { return domain.QuestionSet{}, nil, err } } var synced []domain.QuestionWithDetails if in.Questions != nil { var err error synced, err = s.syncQuestionsInSet(ctx, questionSetID, in.Questions) if err != nil { return domain.QuestionSet{}, nil, err } } set, err := s.questions.GetQuestionSetByID(ctx, questionSetID) if err != nil { return domain.QuestionSet{}, nil, err } if synced == nil && in.Questions == nil { items, err := s.questions.GetQuestionSetItems(ctx, questionSetID) if err != nil { return domain.QuestionSet{}, nil, err } synced = make([]domain.QuestionWithDetails, 0, len(items)) for _, item := range items { q, err := s.questions.GetQuestionWithDetails(ctx, item.QuestionID) if err != nil { return domain.QuestionSet{}, nil, err } synced = append(synced, q) } } return set, synced, nil } // UpdateLmsPracticeFull updates an LMS practice shell, linked question set, and questions. func (s *Service) UpdateLmsPracticeFull(ctx context.Context, practiceID int64, in domain.FullUpdatePracticeInput) (domain.FullUpdatePracticeResult, error) { practice, err := s.lms.GetLmsPracticeByID(ctx, practiceID) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return domain.FullUpdatePracticeResult{}, ErrPracticeNotFound } return domain.FullUpdatePracticeResult{}, err } questionSetID := practice.QuestionSetID if in.Practice != nil { if in.Practice.PersonaID != nil { if err := s.validatePersona(ctx, *in.Practice.PersonaID); err != nil { return domain.FullUpdatePracticeResult{}, err } } if in.Practice.QuestionSetID != nil { if err := s.validateQuestionSet(ctx, *in.Practice.QuestionSetID); err != nil { return domain.FullUpdatePracticeResult{}, err } } update := domain.UpdatePracticeInput{ Title: in.Practice.Title, StoryDescription: in.Practice.StoryDescription, StoryImage: in.Practice.StoryImage, PersonaID: in.Practice.PersonaID, QuestionSetID: in.Practice.QuestionSetID, QuickTips: in.Practice.QuickTips, PublishStatus: in.Practice.PublishStatus, } practice, err = s.lms.UpdateLmsPractice(ctx, practiceID, update) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return domain.FullUpdatePracticeResult{}, ErrPracticeNotFound } return domain.FullUpdatePracticeResult{}, err } questionSetID = practice.QuestionSetID } set, questions, err := s.applySharedUpdates(ctx, questionSetID, in) if err != nil { return domain.FullUpdatePracticeResult{}, err } return domain.FullUpdatePracticeResult{ Practice: practice, QuestionSet: set, Questions: questions, }, nil } // UpdateExamPrepPracticeFull updates an exam-prep practice shell, linked question set, and questions. func (s *Service) UpdateExamPrepPracticeFull(ctx context.Context, practiceID int64, in domain.FullUpdatePracticeInput) (domain.FullUpdatePracticeResult, error) { practice, err := s.examPrep.GetExamPrepLessonPracticeByID(ctx, practiceID) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return domain.FullUpdatePracticeResult{}, ErrPracticeNotFound } return domain.FullUpdatePracticeResult{}, err } questionSetID := practice.QuestionSetID if in.Practice != nil { if in.Practice.PersonaID != nil { if err := s.validatePersona(ctx, *in.Practice.PersonaID); err != nil { return domain.FullUpdatePracticeResult{}, err } } if in.Practice.QuestionSetID != nil { if err := s.validateQuestionSet(ctx, *in.Practice.QuestionSetID); err != nil { return domain.FullUpdatePracticeResult{}, err } } update := domain.UpdateExamPrepPracticeInput{ Title: in.Practice.Title, StoryDescription: in.Practice.StoryDescription, StoryImage: in.Practice.StoryImage, PersonaID: in.Practice.PersonaID, QuestionSetID: in.Practice.QuestionSetID, QuickTips: in.Practice.QuickTips, PublishStatus: in.Practice.PublishStatus, } practice, err = s.examPrep.UpdateExamPrepLessonPractice(ctx, practiceID, update) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return domain.FullUpdatePracticeResult{}, ErrPracticeNotFound } return domain.FullUpdatePracticeResult{}, err } questionSetID = practice.QuestionSetID } set, questions, err := s.applySharedUpdates(ctx, questionSetID, in) if err != nil { return domain.FullUpdatePracticeResult{}, err } return domain.FullUpdatePracticeResult{ Practice: practice, QuestionSet: set, Questions: questions, }, nil }