package modules import ( "Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/ports" "Yimaru-Backend/internal/services/courses" "context" "errors" "github.com/jackc/pgx/v5" ) var ErrModuleNotFound = errors.New("module not found") type Service struct { modules ports.ModuleStore courses ports.CourseStore } func NewService(modules ports.ModuleStore, courses ports.CourseStore) *Service { return &Service{modules: modules, courses: courses} } func (s *Service) getCourseOrErr(ctx context.Context, courseID int64) (domain.Course, error) { c, err := s.courses.GetCourseByID(ctx, courseID) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return domain.Course{}, courses.ErrCourseNotFound } return domain.Course{}, err } return c, nil } // Create loads the course and stores program_id from it (parent program is not taken from the URL). func (s *Service) Create(ctx context.Context, courseID int64, input domain.CreateModuleInput) (domain.Module, error) { c, err := s.getCourseOrErr(ctx, courseID) if err != nil { return domain.Module{}, err } return s.modules.CreateModule(ctx, c.ProgramID, courseID, input) } func (s *Service) GetByID(ctx context.Context, id int64) (domain.Module, error) { m, err := s.modules.GetModuleByID(ctx, id) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return domain.Module{}, ErrModuleNotFound } return domain.Module{}, err } return m, nil } // ListByCourse loads the course and lists modules for its program_id and course_id. func (s *Service) ListByCourse(ctx context.Context, courseID int64, limit, offset int32) ([]domain.Module, int64, error) { c, err := s.getCourseOrErr(ctx, courseID) if err != nil { return nil, 0, err } if limit <= 0 { limit = 20 } if limit > 200 { limit = 200 } if offset < 0 { offset = 0 } return s.modules.ListModulesByProgramAndCourse(ctx, c.ProgramID, courseID, limit, offset) } func (s *Service) Update(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error) { m, err := s.modules.UpdateModule(ctx, id, input) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return domain.Module{}, ErrModuleNotFound } return domain.Module{}, err } return m, nil } func (s *Service) Delete(ctx context.Context, id int64) error { if _, err := s.modules.GetModuleByID(ctx, id); err != nil { if errors.Is(err, pgx.ErrNoRows) { return ErrModuleNotFound } return err } return s.modules.DeleteModule(ctx, id) } // ReorderInCourse sets module sort_order under a course. ordered must list every module id in that course // exactly once (e.g. from GET /courses/{id}/modules) in the desired order. func (s *Service) ReorderInCourse(ctx context.Context, courseID int64, ordered []int64) error { if _, err := s.getCourseOrErr(ctx, courseID); err != nil { return err } expected, err := s.modules.ListModuleIDsByCourse(ctx, courseID) if err != nil { return err } if err := domain.ValidateReorderPermutation(ordered, expected); err != nil { return err } if len(ordered) == 0 { return nil } return s.modules.ReorderModulesInCourse(ctx, courseID, ordered) }