Yimaru-BackEnd/internal/services/assessment/initial_assessment.go

182 lines
4.2 KiB
Go

package assessment
import (
"Yimaru-Backend/internal/domain"
"context"
"errors"
"time"
)
func (s *Service) GetActiveAssessmentQuestions(
ctx context.Context,
) ([]domain.AssessmentQuestion, error) {
questions, err := s.initialAssessmentStore.GetActiveAssessmentQuestions(ctx)
if err != nil {
return nil, err
}
// IMPORTANT:
// Do NOT expose correct answers to the client
for i := range questions {
for j := range questions[i].Options {
questions[i].Options[j].IsCorrect = false
}
}
return questions, nil
}
func (s *Service) CreateAssessmentQuestion(
ctx context.Context,
q domain.AssessmentQuestion,
) (domain.AssessmentQuestion, error) {
// Basic validation
if q.Title == "" {
return domain.AssessmentQuestion{}, errors.New("question title is required")
}
if q.QuestionType == "" {
return domain.AssessmentQuestion{}, errors.New("question type is required")
}
if q.DifficultyLevel == "" {
return domain.AssessmentQuestion{}, errors.New("difficulty level is required")
}
// Multiple choice / true-false must have options
if q.QuestionType != string(domain.QuestionTypeShortAnswer) {
if len(q.Options) < 2 {
return domain.AssessmentQuestion{}, errors.New("at least two options are required")
}
hasCorrect := false
for _, opt := range q.Options {
if opt.OptionText == "" {
return domain.AssessmentQuestion{}, errors.New("option text cannot be empty")
}
if opt.IsCorrect {
hasCorrect = true
}
}
if !hasCorrect {
return domain.AssessmentQuestion{}, errors.New("at least one correct option is required")
}
}
// Persist via repository
return s.initialAssessmentStore.CreateAssessmentQuestion(ctx, q)
}
func (s *Service) SubmitAssessment(
ctx context.Context,
userID int64,
responses []domain.UserAnswer,
) (domain.AssessmentAttempt, error) {
if userID <= 0 {
return domain.AssessmentAttempt{}, errors.New("invalid user id")
}
if len(responses) == 0 {
return domain.AssessmentAttempt{}, errors.New("no responses submitted")
}
// Step 1: Validate and evaluate answers
for i, ans := range responses {
if ans.QuestionID == 0 {
return domain.AssessmentAttempt{}, errors.New("invalid question id")
}
isCorrect, err := s.validateAnswer(ctx, ans)
if err != nil {
return domain.AssessmentAttempt{}, err
}
responses[i].IsCorrect = isCorrect
}
// Step 2: Persist assessment attempt + answers
attempt, err := s.initialAssessmentStore.SaveAssessmentAttempt(
ctx,
userID,
responses,
)
if err != nil {
return domain.AssessmentAttempt{}, err
}
// Step 3: Update user's knowledge level
if err := s.userStore.UpdateUserKnowledgeLevel(
ctx,
userID,
attempt.KnowledgeLevel,
); err != nil {
return domain.AssessmentAttempt{}, err
}
// Step 4: Send in-app notification
notification := &domain.Notification{
RecipientID: userID,
Level: domain.NotificationLevelInfo,
Reciever: domain.NotificationRecieverSideCustomer,
IsRead: false,
DeliveryStatus: domain.DeliveryStatusSent,
DeliveryChannel: domain.DeliveryChannelInApp,
Payload: domain.NotificationPayload{
Headline: "Knowledge Assessment Completed",
Message: "Your knowledge assessment is complete. Your knowledge level is " + attempt.KnowledgeLevel + ".",
Tags: []string{"assessment", "knowledge-level"},
},
Timestamp: time.Now(),
Type: domain.NOTIFICATION_TYPE_KNOWLEDGE_LEVEL_UPDATE,
}
if err := s.notificationSvc.SendNotification(ctx, notification); err != nil {
return domain.AssessmentAttempt{}, err
}
return attempt, nil
}
func (s *Service) validateAnswer(
ctx context.Context,
answer domain.UserAnswer,
) (bool, error) {
// Multiple choice / True-False
if answer.SelectedOptionID != 0 {
option, err := s.initialAssessmentStore.GetOptionByID(
ctx,
answer.SelectedOptionID,
)
if err != nil {
return false, err
}
return option.IsCorrect, nil
}
// Short answer (future-proofing)
if answer.ShortAnswer != "" {
// Placeholder: subjective/manual evaluation
// For now, mark incorrect
return false, nil
}
return false, errors.New("invalid answer submission")
}
func CalculateKnowledgeLevel(score float64) string {
switch {
case score >= 80:
return "ADVANCED"
case score >= 50:
return "INTERMEDIATE"
default:
return "BEGINNER"
}
}