Yimaru-BackEnd/internal/services/practices/service.go
Yared Yemane 12ad59c409 Add draft vs published status for LMS and exam-prep practices.
Expose publish_status on create/update, filter learner-facing lists and gates, and add migration 000060.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 03:57:43 -07:00

209 lines
5.9 KiB
Go

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)
}