package practices import ( "Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/ports" "Yimaru-Backend/internal/services/courses" "Yimaru-Backend/internal/services/lessons" "Yimaru-Backend/internal/services/modules" "context" "errors" "github.com/jackc/pgx/v5" ) var ( ErrPracticeNotFound = errors.New("practice not found") ErrQuestionSetNotFound = errors.New("question set not found") ErrInvalidPracticeParent = errors.New("parent_kind and parent_id do not match an allowed parent") ) type Service struct { practices ports.LmsPracticeStore courses ports.CourseStore modules ports.ModuleStore lessons ports.LessonStore qs ports.QuestionSetByID users ports.UserByID } func NewService( practices ports.LmsPracticeStore, courses ports.CourseStore, modules ports.ModuleStore, lessons ports.LessonStore, qs ports.QuestionSetByID, users ports.UserByID, ) *Service { return &Service{ practices: practices, courses: courses, modules: modules, lessons: lessons, qs: qs, users: users, } } func (s *Service) validateQuestionSet(ctx context.Context, id int64) error { _, err := s.qs.GetQuestionSetByID(ctx, id) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return ErrQuestionSetNotFound } return err } return nil } func (s *Service) validatePersonaUser(ctx context.Context, id int64) error { _, err := s.users.GetUserByID(ctx, id) if err != nil { if errors.Is(err, domain.ErrUserNotFound) { return domain.ErrUserNotFound } return err } return nil } func (s *Service) resolveParent(ctx context.Context, in domain.CreatePracticeInput) (courseID, moduleID, lessonID *int64, err error) { pid := in.ParentID switch in.ParentKind { case domain.ParentKindCourse: if _, e := s.courses.GetCourseByID(ctx, pid); e != nil { if errors.Is(e, pgx.ErrNoRows) { return nil, nil, nil, courses.ErrCourseNotFound } return nil, nil, nil, e } return &pid, nil, nil, nil case domain.ParentKindModule: if _, e := s.modules.GetModuleByID(ctx, pid); e != nil { if errors.Is(e, pgx.ErrNoRows) { return nil, nil, nil, modules.ErrModuleNotFound } return nil, nil, nil, e } return nil, &pid, nil, nil case domain.ParentKindLesson: if _, e := s.lessons.GetLessonByID(ctx, pid); e != nil { if errors.Is(e, pgx.ErrNoRows) { return nil, nil, nil, lessons.ErrLessonNotFound } return nil, nil, nil, e } return nil, nil, &pid, nil default: return nil, nil, nil, ErrInvalidPracticeParent } } func (s *Service) Create(ctx context.Context, in domain.CreatePracticeInput) (domain.Practice, error) { if err := s.validateQuestionSet(ctx, in.QuestionSetID); err != nil { return domain.Practice{}, err } if in.PersonaID != nil { if err := s.validatePersonaUser(ctx, *in.PersonaID); err != nil { return domain.Practice{}, err } } courseID, moduleID, lessonID, err := s.resolveParent(ctx, in) if err != nil { return domain.Practice{}, err } return s.practices.CreateLmsPractice(ctx, in, courseID, moduleID, lessonID) } func (s *Service) TryGetByQuestionSetID(ctx context.Context, questionSetID int64) (domain.Practice, bool, error) { return s.practices.TryGetLmsPracticeByQuestionSetID(ctx, questionSetID) } func (s *Service) GetByID(ctx context.Context, id int64) (domain.Practice, error) { p, err := s.practices.GetLmsPracticeByID(ctx, id) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return domain.Practice{}, ErrPracticeNotFound } return domain.Practice{}, err } return p, nil } func clampPracticePage(limit, offset int32) (int32, int32) { if limit <= 0 { limit = 20 } if limit > 200 { limit = 200 } if offset < 0 { offset = 0 } return limit, offset } func (s *Service) ListByCourse(ctx context.Context, courseID int64, publishedOnly bool, limit, offset int32) ([]domain.Practice, int64, error) { if _, err := s.courses.GetCourseByID(ctx, courseID); err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, 0, courses.ErrCourseNotFound } return nil, 0, err } limit, offset = clampPracticePage(limit, offset) return s.practices.ListLmsPracticesByCourseID(ctx, courseID, publishedOnly, limit, offset) } func (s *Service) ListByModule(ctx context.Context, moduleID int64, publishedOnly bool, limit, offset int32) ([]domain.Practice, int64, error) { if _, err := s.modules.GetModuleByID(ctx, moduleID); err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, 0, modules.ErrModuleNotFound } return nil, 0, err } limit, offset = clampPracticePage(limit, offset) return s.practices.ListLmsPracticesByModuleID(ctx, moduleID, publishedOnly, limit, offset) } func (s *Service) ListByLesson(ctx context.Context, lessonID int64, publishedOnly bool, limit, offset int32) ([]domain.Practice, int64, error) { if _, err := s.lessons.GetLessonByID(ctx, lessonID); err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, 0, lessons.ErrLessonNotFound } return nil, 0, err } limit, offset = clampPracticePage(limit, offset) return s.practices.ListLmsPracticesByLessonID(ctx, lessonID, publishedOnly, limit, offset) } func (s *Service) Update(ctx context.Context, id int64, input domain.UpdatePracticeInput) (domain.Practice, error) { if input.QuestionSetID != nil { if err := s.validateQuestionSet(ctx, *input.QuestionSetID); err != nil { return domain.Practice{}, err } } if input.PersonaID != nil { if err := s.validatePersonaUser(ctx, *input.PersonaID); err != nil { return domain.Practice{}, err } } p, err := s.practices.UpdateLmsPractice(ctx, id, input) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return domain.Practice{}, ErrPracticeNotFound } return domain.Practice{}, err } return p, nil } func (s *Service) Delete(ctx context.Context, id int64) error { if _, err := s.practices.GetLmsPracticeByID(ctx, id); err != nil { if errors.Is(err, pgx.ErrNoRows) { return ErrPracticeNotFound } return err } return s.practices.DeleteLmsPractice(ctx, id) }