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>
434 lines
13 KiB
Go
434 lines
13 KiB
Go
package examprep
|
|
|
|
import (
|
|
"Yimaru-Backend/internal/domain"
|
|
"Yimaru-Backend/internal/ports"
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
var ErrCatalogCourseNotFound = errors.New("exam prep catalog course not found")
|
|
var ErrUnitNotFound = errors.New("exam prep unit not found")
|
|
var ErrModuleNotFound = errors.New("exam prep module not found")
|
|
var ErrLessonNotFound = errors.New("exam prep lesson not found")
|
|
var ErrPracticeNotFound = errors.New("exam prep practice not found")
|
|
|
|
// examPrepStore is implemented by *repository.Store (catalog courses, units, modules, lessons, practices, personas).
|
|
type examPrepStore interface {
|
|
ports.ExamPrepCatalogCourseStore
|
|
ports.ExamPrepUnitStore
|
|
ports.ExamPrepModuleStore
|
|
ports.ExamPrepLessonStore
|
|
ports.ExamPrepPracticeStore
|
|
ports.LmsPersonaReader
|
|
}
|
|
|
|
type Service struct {
|
|
store examPrepStore
|
|
}
|
|
|
|
func NewService(store examPrepStore) *Service {
|
|
return &Service{store: store}
|
|
}
|
|
|
|
func (s *Service) ensurePersonaRef(ctx context.Context, id int64) error {
|
|
if id <= 0 {
|
|
return domain.ErrPersonaNotFound
|
|
}
|
|
_, err := s.store.GetLmsPersonaByID(ctx, id)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ErrPersonaNotFound
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s *Service) CreateCatalogCourse(ctx context.Context, input domain.CreateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error) {
|
|
return s.store.CreateExamPrepCatalogCourse(ctx, input)
|
|
}
|
|
|
|
func (s *Service) GetCatalogCourseByID(ctx context.Context, id int64) (domain.ExamPrepCatalogCourse, error) {
|
|
c, err := s.store.GetExamPrepCatalogCourseByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ExamPrepCatalogCourse{}, ErrCatalogCourseNotFound
|
|
}
|
|
return domain.ExamPrepCatalogCourse{}, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func (s *Service) ListCatalogCourses(ctx context.Context, limit, offset int32) ([]domain.ExamPrepCatalogCourse, int64, error) {
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
if limit > 200 {
|
|
limit = 200
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
return s.store.ListExamPrepCatalogCourses(ctx, limit, offset)
|
|
}
|
|
|
|
func (s *Service) UpdateCatalogCourse(ctx context.Context, id int64, input domain.UpdateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error) {
|
|
c, err := s.store.UpdateExamPrepCatalogCourse(ctx, id, input)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ExamPrepCatalogCourse{}, ErrCatalogCourseNotFound
|
|
}
|
|
return domain.ExamPrepCatalogCourse{}, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func (s *Service) DeleteCatalogCourse(ctx context.Context, id int64) error {
|
|
if _, err := s.store.GetExamPrepCatalogCourseByID(ctx, id); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return ErrCatalogCourseNotFound
|
|
}
|
|
return err
|
|
}
|
|
return s.store.DeleteExamPrepCatalogCourse(ctx, id)
|
|
}
|
|
|
|
func (s *Service) ReorderCatalogCourses(ctx context.Context, ordered []int64) error {
|
|
expected, err := s.store.ListAllExamPrepCatalogCourseIDs(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := domain.ValidateReorderPermutation(ordered, expected); err != nil {
|
|
return err
|
|
}
|
|
if len(ordered) == 0 {
|
|
return nil
|
|
}
|
|
return s.store.ReorderExamPrepCatalogCourses(ctx, ordered)
|
|
}
|
|
|
|
func (s *Service) ensureCatalogCourse(ctx context.Context, catalogCourseID int64) error {
|
|
if _, err := s.store.GetExamPrepCatalogCourseByID(ctx, catalogCourseID); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return ErrCatalogCourseNotFound
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) CreateUnit(ctx context.Context, catalogCourseID int64, input domain.CreateExamPrepUnitInput) (domain.ExamPrepUnit, error) {
|
|
if err := s.ensureCatalogCourse(ctx, catalogCourseID); err != nil {
|
|
return domain.ExamPrepUnit{}, err
|
|
}
|
|
return s.store.CreateExamPrepUnit(ctx, catalogCourseID, input)
|
|
}
|
|
|
|
func (s *Service) ListUnitsByCatalogCourse(ctx context.Context, catalogCourseID int64, limit, offset int32) ([]domain.ExamPrepUnit, int64, error) {
|
|
if err := s.ensureCatalogCourse(ctx, catalogCourseID); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
if limit > 200 {
|
|
limit = 200
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
return s.store.ListExamPrepUnitsByCatalogCourse(ctx, catalogCourseID, limit, offset)
|
|
}
|
|
|
|
func (s *Service) GetUnitByID(ctx context.Context, id int64) (domain.ExamPrepUnit, error) {
|
|
u, err := s.store.GetExamPrepUnitByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ExamPrepUnit{}, ErrUnitNotFound
|
|
}
|
|
return domain.ExamPrepUnit{}, err
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
func (s *Service) UpdateUnit(ctx context.Context, id int64, input domain.UpdateExamPrepUnitInput) (domain.ExamPrepUnit, error) {
|
|
u, err := s.store.UpdateExamPrepUnit(ctx, id, input)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ExamPrepUnit{}, ErrUnitNotFound
|
|
}
|
|
return domain.ExamPrepUnit{}, err
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
func (s *Service) DeleteUnit(ctx context.Context, id int64) error {
|
|
if _, err := s.store.GetExamPrepUnitByID(ctx, id); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return ErrUnitNotFound
|
|
}
|
|
return err
|
|
}
|
|
return s.store.DeleteExamPrepUnit(ctx, id)
|
|
}
|
|
|
|
func (s *Service) ReorderUnitsInCatalogCourse(ctx context.Context, catalogCourseID int64, ordered []int64) error {
|
|
if err := s.ensureCatalogCourse(ctx, catalogCourseID); err != nil {
|
|
return err
|
|
}
|
|
expected, err := s.store.ListExamPrepUnitIDsByCatalogCourse(ctx, catalogCourseID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := domain.ValidateReorderPermutation(ordered, expected); err != nil {
|
|
return err
|
|
}
|
|
if len(ordered) == 0 {
|
|
return nil
|
|
}
|
|
return s.store.ReorderExamPrepUnitsInCatalogCourse(ctx, catalogCourseID, ordered)
|
|
}
|
|
|
|
func (s *Service) ensureUnit(ctx context.Context, unitID int64) error {
|
|
if _, err := s.store.GetExamPrepUnitByID(ctx, unitID); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return ErrUnitNotFound
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) CreateModule(ctx context.Context, unitID int64, input domain.CreateExamPrepModuleInput) (domain.ExamPrepModule, error) {
|
|
if err := s.ensureUnit(ctx, unitID); err != nil {
|
|
return domain.ExamPrepModule{}, err
|
|
}
|
|
return s.store.CreateExamPrepUnitModule(ctx, unitID, input)
|
|
}
|
|
|
|
func (s *Service) ListModulesByUnit(ctx context.Context, unitID int64, limit, offset int32) ([]domain.ExamPrepModule, int64, error) {
|
|
if err := s.ensureUnit(ctx, unitID); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
if limit > 200 {
|
|
limit = 200
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
return s.store.ListExamPrepUnitModulesByUnit(ctx, unitID, limit, offset)
|
|
}
|
|
|
|
func (s *Service) GetModuleByID(ctx context.Context, id int64) (domain.ExamPrepModule, error) {
|
|
m, err := s.store.GetExamPrepUnitModuleByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ExamPrepModule{}, ErrModuleNotFound
|
|
}
|
|
return domain.ExamPrepModule{}, err
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (s *Service) UpdateModule(ctx context.Context, id int64, input domain.UpdateExamPrepModuleInput) (domain.ExamPrepModule, error) {
|
|
m, err := s.store.UpdateExamPrepUnitModule(ctx, id, input)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ExamPrepModule{}, ErrModuleNotFound
|
|
}
|
|
return domain.ExamPrepModule{}, err
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (s *Service) DeleteModule(ctx context.Context, id int64) error {
|
|
if _, err := s.store.GetExamPrepUnitModuleByID(ctx, id); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return ErrModuleNotFound
|
|
}
|
|
return err
|
|
}
|
|
return s.store.DeleteExamPrepUnitModule(ctx, id)
|
|
}
|
|
|
|
func (s *Service) ReorderModulesInUnit(ctx context.Context, unitID int64, ordered []int64) error {
|
|
if err := s.ensureUnit(ctx, unitID); err != nil {
|
|
return err
|
|
}
|
|
expected, err := s.store.ListExamPrepUnitModuleIDsByUnit(ctx, unitID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := domain.ValidateReorderPermutation(ordered, expected); err != nil {
|
|
return err
|
|
}
|
|
if len(ordered) == 0 {
|
|
return nil
|
|
}
|
|
return s.store.ReorderExamPrepUnitModulesInUnit(ctx, unitID, ordered)
|
|
}
|
|
|
|
func (s *Service) ensureModule(ctx context.Context, unitModuleID int64) error {
|
|
if _, err := s.store.GetExamPrepUnitModuleByID(ctx, unitModuleID); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return ErrModuleNotFound
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) CreateLesson(ctx context.Context, unitModuleID int64, input domain.CreateExamPrepLessonInput) (domain.ExamPrepLesson, error) {
|
|
if err := s.ensureModule(ctx, unitModuleID); err != nil {
|
|
return domain.ExamPrepLesson{}, err
|
|
}
|
|
return s.store.CreateExamPrepUnitModuleLesson(ctx, unitModuleID, input)
|
|
}
|
|
|
|
func (s *Service) ListLessonsByUnitModule(ctx context.Context, unitModuleID int64, limit, offset int32) ([]domain.ExamPrepLesson, int64, error) {
|
|
if err := s.ensureModule(ctx, unitModuleID); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
if limit > 200 {
|
|
limit = 200
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
return s.store.ListExamPrepUnitModuleLessonsByUnitModuleID(ctx, unitModuleID, limit, offset)
|
|
}
|
|
|
|
func (s *Service) GetLessonByID(ctx context.Context, id int64) (domain.ExamPrepLesson, error) {
|
|
l, err := s.store.GetExamPrepUnitModuleLessonByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ExamPrepLesson{}, ErrLessonNotFound
|
|
}
|
|
return domain.ExamPrepLesson{}, err
|
|
}
|
|
return l, nil
|
|
}
|
|
|
|
func (s *Service) UpdateLesson(ctx context.Context, id int64, input domain.UpdateExamPrepLessonInput) (domain.ExamPrepLesson, error) {
|
|
l, err := s.store.UpdateExamPrepUnitModuleLesson(ctx, id, input)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ExamPrepLesson{}, ErrLessonNotFound
|
|
}
|
|
return domain.ExamPrepLesson{}, err
|
|
}
|
|
return l, nil
|
|
}
|
|
|
|
func (s *Service) DeleteLesson(ctx context.Context, id int64) error {
|
|
if _, err := s.store.GetExamPrepUnitModuleLessonByID(ctx, id); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return ErrLessonNotFound
|
|
}
|
|
return err
|
|
}
|
|
return s.store.DeleteExamPrepUnitModuleLesson(ctx, id)
|
|
}
|
|
|
|
func (s *Service) ReorderLessonsInUnitModule(ctx context.Context, unitModuleID int64, ordered []int64) error {
|
|
if err := s.ensureModule(ctx, unitModuleID); err != nil {
|
|
return err
|
|
}
|
|
expected, err := s.store.ListExamPrepUnitModuleLessonIDsByUnitModule(ctx, unitModuleID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := domain.ValidateReorderPermutation(ordered, expected); err != nil {
|
|
return err
|
|
}
|
|
if len(ordered) == 0 {
|
|
return nil
|
|
}
|
|
return s.store.ReorderExamPrepUnitModuleLessonsInUnitModule(ctx, unitModuleID, ordered)
|
|
}
|
|
|
|
func (s *Service) ensureLesson(ctx context.Context, lessonID int64) error {
|
|
if _, err := s.store.GetExamPrepUnitModuleLessonByID(ctx, lessonID); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return ErrLessonNotFound
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) CreateExamPrepPractice(ctx context.Context, lessonID int64, input domain.CreateExamPrepPracticeInput) (domain.ExamPrepPractice, error) {
|
|
if err := s.ensureLesson(ctx, lessonID); err != nil {
|
|
return domain.ExamPrepPractice{}, err
|
|
}
|
|
if input.PersonaID != nil {
|
|
if err := s.ensurePersonaRef(ctx, *input.PersonaID); err != nil {
|
|
return domain.ExamPrepPractice{}, err
|
|
}
|
|
}
|
|
return s.store.CreateExamPrepLessonPractice(ctx, lessonID, input)
|
|
}
|
|
|
|
func (s *Service) ListExamPrepPracticesByLesson(ctx context.Context, lessonID int64, publishedOnly bool, limit, offset int32) ([]domain.ExamPrepPractice, int64, error) {
|
|
if err := s.ensureLesson(ctx, lessonID); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
if limit > 200 {
|
|
limit = 200
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
return s.store.ListExamPrepLessonPracticesByLessonID(ctx, lessonID, publishedOnly, limit, offset)
|
|
}
|
|
|
|
func (s *Service) GetExamPrepPracticeByID(ctx context.Context, id int64) (domain.ExamPrepPractice, error) {
|
|
p, err := s.store.GetExamPrepLessonPracticeByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ExamPrepPractice{}, ErrPracticeNotFound
|
|
}
|
|
return domain.ExamPrepPractice{}, err
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func (s *Service) TryGetExamPrepPracticeByQuestionSetID(ctx context.Context, questionSetID int64) (domain.ExamPrepPractice, bool, error) {
|
|
return s.store.TryGetExamPrepLessonPracticeByQuestionSetID(ctx, questionSetID)
|
|
}
|
|
|
|
func (s *Service) UpdateExamPrepPractice(ctx context.Context, id int64, input domain.UpdateExamPrepPracticeInput) (domain.ExamPrepPractice, error) {
|
|
if input.PersonaID != nil {
|
|
if err := s.ensurePersonaRef(ctx, *input.PersonaID); err != nil {
|
|
return domain.ExamPrepPractice{}, err
|
|
}
|
|
}
|
|
p, err := s.store.UpdateExamPrepLessonPractice(ctx, id, input)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ExamPrepPractice{}, ErrPracticeNotFound
|
|
}
|
|
return domain.ExamPrepPractice{}, err
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func (s *Service) DeleteExamPrepPractice(ctx context.Context, id int64) error {
|
|
if _, err := s.store.GetExamPrepLessonPracticeByID(ctx, id); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return ErrPracticeNotFound
|
|
}
|
|
return err
|
|
}
|
|
return s.store.DeleteExamPrepLessonPractice(ctx, id)
|
|
}
|