182 lines
4.2 KiB
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"
|
|
}
|
|
}
|