Yimaru-BackEnd/internal/services/practicecontent/service.go
Yared Yemane 22464479ae feat: add full practice update endpoints for LMS and exam prep
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>
2026-06-08 02:43:34 -07:00

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
}