package assignmentgen import ( "context" "errors" "fmt" "strings" "boostai-backend/internal/questiongen" "boostai-backend/internal/sqlc" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) func (s *Service) validateGenerateRequest(params GenerateStudentQuestionSetParams) error { if s == nil || s.db == nil || s.db.Pool == nil { return errors.New("assignment question generator database is not configured") } if s.generator == nil { return errors.New("assignment question generator is not configured") } if params.AssignmentID <= 0 || params.StudentID <= 0 || params.TeacherID <= 0 { return errors.New("assignment_id, student_id, and teacher_id are required") } return validatePlanItems(params.Plan) } func validatePlanItems(plan []PlanItem) error { if len(plan) == 0 { return errors.New("at least one generation plan item is required") } for _, item := range plan { if item.Count <= 0 { return fmt.Errorf("generation count must be positive for bucket %q", item.SourceBucket) } if strings.TrimSpace(string(item.SourceBucket)) == "" { return errors.New("source bucket is required for every generation plan item") } } return nil } func validateAssignmentOwnership(ctx context.Context, queries *sqlc.Queries, assignmentID, teacherID int64) error { assignment, err := queries.GetAssignmentByID(ctx, assignmentID) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return fmt.Errorf("assignment %d not found", assignmentID) } return err } if assignment.TeacherID != teacherID { return fmt.Errorf("assignment %d does not belong to teacher %d", assignmentID, teacherID) } return nil } func validateStudentAssignment(ctx context.Context, queries *sqlc.Queries, assignmentID, studentID int64) error { _, err := queries.GetAssignmentAssignee(ctx, sqlc.GetAssignmentAssigneeParams{ AssignmentID: assignmentID, StudentID: studentID, }) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return fmt.Errorf("student %d is not assigned to assignment %d", studentID, assignmentID) } return err } return nil } func clearStudentQuestionMappings(ctx context.Context, queries *sqlc.Queries, assignmentID, studentID int64) error { return queries.DeleteAssignmentStudentQuestions(ctx, sqlc.DeleteAssignmentStudentQuestionsParams{ AssignmentID: assignmentID, StudentID: studentID, }) } func normalizeQuestionDefaults(params GenerateStudentQuestionSetParams) (sqlc.QuestionStatus, string) { questionStatus := params.QuestionStatus if questionStatus == "" { questionStatus = sqlc.QuestionStatusDraft } questionSource := strings.TrimSpace(params.QuestionSource) if questionSource == "" { questionSource = defaultQuestionSource } return questionStatus, questionSource } func (s *Service) generateAndStorePlan( ctx context.Context, queries *sqlc.Queries, params GenerateStudentQuestionSetParams, questionStatus sqlc.QuestionStatus, questionSource string, ) ([]StoredStudentQuestion, error) { stored := make([]StoredStudentQuestion, 0) position := int32(1) for _, item := range params.Plan { generatedQuestions, usedSeed, err := s.generator.Generate(questiongen.GenerateParams{ Topic: item.Topic, Difficulty: item.Difficulty, Count: item.Count, Seed: item.Seed, }) if err != nil { return nil, err } storedBatch, nextPosition, err := storeGeneratedQuestionBatch( ctx, queries, params, item, generatedQuestions, questionStatus, questionSource, usedSeed, position, ) if err != nil { return nil, err } stored = append(stored, storedBatch...) position = nextPosition } return stored, nil } func storeGeneratedQuestionBatch( ctx context.Context, queries *sqlc.Queries, params GenerateStudentQuestionSetParams, item PlanItem, generatedQuestions []questiongen.GeneratedQuestion, questionStatus sqlc.QuestionStatus, questionSource string, usedSeed int64, startPosition int32, ) ([]StoredStudentQuestion, int32, error) { stored := make([]StoredStudentQuestion, 0, len(generatedQuestions)) position := startPosition for _, generated := range generatedQuestions { storedQuestion, err := storeGeneratedQuestion( ctx, queries, params, item, generated, questionStatus, questionSource, usedSeed, position, ) if err != nil { return nil, startPosition, err } stored = append(stored, storedQuestion) position++ } return stored, position, nil } func storeGeneratedQuestion( ctx context.Context, queries *sqlc.Queries, params GenerateStudentQuestionSetParams, item PlanItem, generated questiongen.GeneratedQuestion, questionStatus sqlc.QuestionStatus, questionSource string, usedSeed int64, position int32, ) (StoredStudentQuestion, error) { question, err := queries.CreateQuestion(ctx, sqlc.CreateQuestionParams{ AuthorTeacherID: params.TeacherID, Title: generated.Title, Prompt: generated.Prompt, Topic: nullableQuestionTopic(item.Topic), Subject: textValue(firstNonEmpty(params.Subject, questionTopicLabel(item.Topic))), Difficulty: nullableQuestionDifficulty(item.Difficulty), Source: textValue(questionSource), Status: questionStatus, CorrectAnswer: textValue(generated.CorrectAnswer), }) if err != nil { return StoredStudentQuestion{}, err } tags := mergeTags(generated.Tags, string(item.SourceBucket), questionSource) if err := attachQuestionTags(ctx, queries, question.ID, tags); err != nil { return StoredStudentQuestion{}, err } mapping, err := queries.AddAssignmentStudentQuestion(ctx, sqlc.AddAssignmentStudentQuestionParams{ AssignmentID: params.AssignmentID, StudentID: params.StudentID, QuestionID: question.ID, Position: position, SourceBucket: string(item.SourceBucket), SourceTopic: nullableQuestionTopic(item.Topic), SourceDifficulty: nullableQuestionDifficulty(item.Difficulty), GeneratorSeed: pgtype.Int8{Int64: usedSeed, Valid: true}, }) if err != nil { return StoredStudentQuestion{}, err } return StoredStudentQuestion{ Mapping: mapping, Question: question, Tags: tags, UsedSeed: usedSeed, SourceBucket: string(item.SourceBucket), }, nil } func attachQuestionTags(ctx context.Context, queries *sqlc.Queries, questionID int64, tagNames []string) error { for _, tagName := range tagNames { tag, err := queries.CreateTag(ctx, tagName) if err != nil { return err } if err := queries.AttachTagToQuestion(ctx, sqlc.AttachTagToQuestionParams{ QuestionID: questionID, TagID: tag.ID, }); err != nil { return err } } return nil }