PUT /practices/:id/full and PUT /exam-prep/practices/:id/full sync practice shell, question set settings, and questions in one request. Co-authored-by: Cursor <cursoragent@cursor.com>
258 lines
7.3 KiB
Go
258 lines
7.3 KiB
Go
package practicecontent
|
|
|
|
import (
|
|
"Yimaru-Backend/internal/domain"
|
|
"Yimaru-Backend/internal/ports"
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
type Service struct {
|
|
questions ports.QuestionStore
|
|
lms ports.LmsPracticeStore
|
|
examPrep ports.ExamPrepPracticeStore
|
|
personas ports.LmsPersonaReader
|
|
}
|
|
|
|
func NewService(
|
|
questions ports.QuestionStore,
|
|
lms ports.LmsPracticeStore,
|
|
examPrep ports.ExamPrepPracticeStore,
|
|
personas ports.LmsPersonaReader,
|
|
) *Service {
|
|
return &Service{
|
|
questions: questions,
|
|
lms: lms,
|
|
examPrep: examPrep,
|
|
personas: personas,
|
|
}
|
|
}
|
|
|
|
func (s *Service) validatePersona(ctx context.Context, id int64) error {
|
|
if id <= 0 {
|
|
return domain.ErrPersonaNotFound
|
|
}
|
|
_, err := s.personas.GetLmsPersonaByID(ctx, id)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ErrPersonaNotFound
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s *Service) validateQuestionSet(ctx context.Context, id int64) error {
|
|
_, err := s.questions.GetQuestionSetByID(ctx, id)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return ErrQuestionSetNotFound
|
|
}
|
|
return err
|
|
}
|
|
|
|
func mergeQuestionSetInput(existing domain.QuestionSet, in *domain.FullUpdateQuestionSetInput) domain.CreateQuestionSetInput {
|
|
out := domain.CreateQuestionSetInput{
|
|
Title: existing.Title,
|
|
Description: existing.Description,
|
|
SetType: existing.SetType,
|
|
OwnerType: existing.OwnerType,
|
|
OwnerID: existing.OwnerID,
|
|
BannerImage: existing.BannerImage,
|
|
Persona: existing.Persona,
|
|
TimeLimitMinutes: existing.TimeLimitMinutes,
|
|
PassingScore: existing.PassingScore,
|
|
ShuffleQuestions: &existing.ShuffleQuestions,
|
|
Status: &existing.Status,
|
|
IntroVideoURL: existing.IntroVideoURL,
|
|
}
|
|
if in == nil {
|
|
return out
|
|
}
|
|
if in.Title != nil {
|
|
out.Title = *in.Title
|
|
}
|
|
if in.Description != nil {
|
|
out.Description = in.Description
|
|
}
|
|
if in.BannerImage != nil {
|
|
out.BannerImage = in.BannerImage
|
|
}
|
|
if in.Persona != nil {
|
|
out.Persona = in.Persona
|
|
}
|
|
if in.TimeLimitMinutes != nil {
|
|
out.TimeLimitMinutes = in.TimeLimitMinutes
|
|
}
|
|
if in.PassingScore != nil {
|
|
out.PassingScore = in.PassingScore
|
|
}
|
|
if in.ShuffleQuestions != nil {
|
|
out.ShuffleQuestions = in.ShuffleQuestions
|
|
}
|
|
if in.Status != nil {
|
|
out.Status = in.Status
|
|
}
|
|
if in.IntroVideoURL != nil {
|
|
out.IntroVideoURL = in.IntroVideoURL
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *Service) applySharedUpdates(
|
|
ctx context.Context,
|
|
questionSetID int64,
|
|
in domain.FullUpdatePracticeInput,
|
|
) (domain.QuestionSet, []domain.QuestionWithDetails, error) {
|
|
if in.QuestionSet != nil {
|
|
existingSet, err := s.questions.GetQuestionSetByID(ctx, questionSetID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.QuestionSet{}, nil, ErrQuestionSetNotFound
|
|
}
|
|
return domain.QuestionSet{}, nil, err
|
|
}
|
|
merged := mergeQuestionSetInput(existingSet, in.QuestionSet)
|
|
if err := s.questions.UpdateQuestionSet(ctx, questionSetID, merged); err != nil {
|
|
return domain.QuestionSet{}, nil, err
|
|
}
|
|
}
|
|
|
|
var synced []domain.QuestionWithDetails
|
|
if in.Questions != nil {
|
|
var err error
|
|
synced, err = s.syncQuestionsInSet(ctx, questionSetID, in.Questions)
|
|
if err != nil {
|
|
return domain.QuestionSet{}, nil, err
|
|
}
|
|
}
|
|
|
|
set, err := s.questions.GetQuestionSetByID(ctx, questionSetID)
|
|
if err != nil {
|
|
return domain.QuestionSet{}, nil, err
|
|
}
|
|
|
|
if synced == nil && in.Questions == nil {
|
|
items, err := s.questions.GetQuestionSetItems(ctx, questionSetID)
|
|
if err != nil {
|
|
return domain.QuestionSet{}, nil, err
|
|
}
|
|
synced = make([]domain.QuestionWithDetails, 0, len(items))
|
|
for _, item := range items {
|
|
q, err := s.questions.GetQuestionWithDetails(ctx, item.QuestionID)
|
|
if err != nil {
|
|
return domain.QuestionSet{}, nil, err
|
|
}
|
|
synced = append(synced, q)
|
|
}
|
|
}
|
|
|
|
return set, synced, nil
|
|
}
|
|
|
|
// UpdateLmsPracticeFull updates an LMS practice shell, linked question set, and questions.
|
|
func (s *Service) UpdateLmsPracticeFull(ctx context.Context, practiceID int64, in domain.FullUpdatePracticeInput) (domain.FullUpdatePracticeResult, error) {
|
|
practice, err := s.lms.GetLmsPracticeByID(ctx, practiceID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.FullUpdatePracticeResult{}, ErrPracticeNotFound
|
|
}
|
|
return domain.FullUpdatePracticeResult{}, err
|
|
}
|
|
|
|
questionSetID := practice.QuestionSetID
|
|
if in.Practice != nil {
|
|
if in.Practice.PersonaID != nil {
|
|
if err := s.validatePersona(ctx, *in.Practice.PersonaID); err != nil {
|
|
return domain.FullUpdatePracticeResult{}, err
|
|
}
|
|
}
|
|
if in.Practice.QuestionSetID != nil {
|
|
if err := s.validateQuestionSet(ctx, *in.Practice.QuestionSetID); err != nil {
|
|
return domain.FullUpdatePracticeResult{}, err
|
|
}
|
|
}
|
|
|
|
update := domain.UpdatePracticeInput{
|
|
Title: in.Practice.Title,
|
|
StoryDescription: in.Practice.StoryDescription,
|
|
StoryImage: in.Practice.StoryImage,
|
|
PersonaID: in.Practice.PersonaID,
|
|
QuestionSetID: in.Practice.QuestionSetID,
|
|
QuickTips: in.Practice.QuickTips,
|
|
PublishStatus: in.Practice.PublishStatus,
|
|
}
|
|
practice, err = s.lms.UpdateLmsPractice(ctx, practiceID, update)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.FullUpdatePracticeResult{}, ErrPracticeNotFound
|
|
}
|
|
return domain.FullUpdatePracticeResult{}, err
|
|
}
|
|
questionSetID = practice.QuestionSetID
|
|
}
|
|
|
|
set, questions, err := s.applySharedUpdates(ctx, questionSetID, in)
|
|
if err != nil {
|
|
return domain.FullUpdatePracticeResult{}, err
|
|
}
|
|
|
|
return domain.FullUpdatePracticeResult{
|
|
Practice: practice,
|
|
QuestionSet: set,
|
|
Questions: questions,
|
|
}, nil
|
|
}
|
|
|
|
// UpdateExamPrepPracticeFull updates an exam-prep practice shell, linked question set, and questions.
|
|
func (s *Service) UpdateExamPrepPracticeFull(ctx context.Context, practiceID int64, in domain.FullUpdatePracticeInput) (domain.FullUpdatePracticeResult, error) {
|
|
practice, err := s.examPrep.GetExamPrepLessonPracticeByID(ctx, practiceID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.FullUpdatePracticeResult{}, ErrPracticeNotFound
|
|
}
|
|
return domain.FullUpdatePracticeResult{}, err
|
|
}
|
|
|
|
questionSetID := practice.QuestionSetID
|
|
if in.Practice != nil {
|
|
if in.Practice.PersonaID != nil {
|
|
if err := s.validatePersona(ctx, *in.Practice.PersonaID); err != nil {
|
|
return domain.FullUpdatePracticeResult{}, err
|
|
}
|
|
}
|
|
if in.Practice.QuestionSetID != nil {
|
|
if err := s.validateQuestionSet(ctx, *in.Practice.QuestionSetID); err != nil {
|
|
return domain.FullUpdatePracticeResult{}, err
|
|
}
|
|
}
|
|
|
|
update := domain.UpdateExamPrepPracticeInput{
|
|
Title: in.Practice.Title,
|
|
StoryDescription: in.Practice.StoryDescription,
|
|
StoryImage: in.Practice.StoryImage,
|
|
PersonaID: in.Practice.PersonaID,
|
|
QuestionSetID: in.Practice.QuestionSetID,
|
|
QuickTips: in.Practice.QuickTips,
|
|
PublishStatus: in.Practice.PublishStatus,
|
|
}
|
|
practice, err = s.examPrep.UpdateExamPrepLessonPractice(ctx, practiceID, update)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.FullUpdatePracticeResult{}, ErrPracticeNotFound
|
|
}
|
|
return domain.FullUpdatePracticeResult{}, err
|
|
}
|
|
questionSetID = practice.QuestionSetID
|
|
}
|
|
|
|
set, questions, err := s.applySharedUpdates(ctx, questionSetID, in)
|
|
if err != nil {
|
|
return domain.FullUpdatePracticeResult{}, err
|
|
}
|
|
|
|
return domain.FullUpdatePracticeResult{
|
|
Practice: practice,
|
|
QuestionSet: set,
|
|
Questions: questions,
|
|
}, nil
|
|
}
|