Introduce lms_personas table, repoint practice persona_id FKs off users, validate persona refs on LMS and exam-prep practice flows, personas.* RBAC permissions, and OpenAPI docs. Co-authored-by: Cursor <cursoragent@cursor.com>
211 lines
6.0 KiB
Go
211 lines
6.0 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
|
|
personas ports.LmsPersonaReader
|
|
}
|
|
|
|
func NewService(
|
|
practices ports.LmsPracticeStore,
|
|
courses ports.CourseStore,
|
|
modules ports.ModuleStore,
|
|
lessons ports.LessonStore,
|
|
qs ports.QuestionSetByID,
|
|
personas ports.LmsPersonaReader,
|
|
) *Service {
|
|
return &Service{
|
|
practices: practices,
|
|
courses: courses,
|
|
modules: modules,
|
|
lessons: lessons,
|
|
qs: qs,
|
|
personas: personas,
|
|
}
|
|
}
|
|
|
|
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) validatePersonaCatalog(ctx context.Context, id int64) error {
|
|
if id <= 0 {
|
|
return domain.ErrPersonaNotFound
|
|
}
|
|
_, err := s.personas.GetLmsPersonaByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ErrPersonaNotFound
|
|
}
|
|
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.validatePersonaCatalog(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.validatePersonaCatalog(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)
|
|
}
|