package service import ( "errors" "fmt" "time" "dating-app/internal/dto" "dating-app/internal/models" "dating-app/internal/repository" ) var ( ErrStepOutOfOrder = errors.New("لطفاً ابتدا مراحل قبلی را تکمیل کنید") ErrInvalidBirthDate = errors.New("فرمت تاریخ تولد نامعتبر است (YYYY-MM-DD)") ErrOptionsNotFound = errors.New("برخی از گزینه‌های انتخابی معتبر نیستند") ErrSelectionCount = errors.New("تعداد گزینه‌های انتخابی مجاز نیست") ErrQuestionsInvalid = errors.New("سوالات انتخابی معتبر نیستند") ErrDuplicateQuestion = errors.New("هر سوال فقط یک‌بار می‌تواند انتخاب شود") ) type OnboardingService struct { userRepo *repository.UserRepository optionRepo *repository.OptionRepository questionRepo *repository.QuestionRepository } func NewOnboardingService( userRepo *repository.UserRepository, optionRepo *repository.OptionRepository, questionRepo *repository.QuestionRepository, ) *OnboardingService { return &OnboardingService{ userRepo: userRepo, optionRepo: optionRepo, questionRepo: questionRepo, } } // canModifyStep بررسی می‌کند که آیا کاربر مجاز به تکمیل/ویرایش این مرحله هست یا نه. // اگر کاربر قبلاً کل آنبوردینگ رو تموم کرده، اجازه‌ی ویرایش هر مرحله‌ای رو داره. // در غیر این صورت فقط می‌تونه مرحله‌ی فعلی یا مراحل قبلی (که قبلاً تکمیل شدن) رو بزنه؛ // نمی‌تونه از روی یک مرحله بپره جلو. func (s *OnboardingService) canModifyStep(user *models.User, step models.OnboardingStep) error { if user.IsOnboarded { return nil } if step > user.CurrentStep { return ErrStepOutOfOrder } return nil } // advanceIfCurrent اگه مرحله‌ی تکمیل‌شده همون مرحله‌ی فعلی کاربر بود، به مرحله‌ی بعد می‌ره func (s *OnboardingService) advanceIfCurrent(user *models.User, completedStep models.OnboardingStep) error { if user.CurrentStep != completedStep { // یا مرحله‌ی قبلی داشت ویرایش می‌شد، یا از قبل onboarded بود. // اگر قبلاً توسط ادمین رد شده بود، ویرایش پروفایل یعنی درخواست بررسی مجدد. return s.resubmitIfRejected(user) } next := completedStep + 1 updates := map[string]interface{}{"current_step": next} if next >= models.StepCompleted { updates["current_step"] = models.StepCompleted updates["is_onboarded"] = true updates["approval_status"] = models.ApprovalPending updates["rejection_reason"] = nil } return s.userRepo.UpdateFields(user, updates) } // resubmitIfRejected اگه کاربر قبلاً رد شده و بعد از onboarded شدن پروفایلش رو // ویرایش کنه، وضعیت رو به pending برمی‌گردونه تا ادمین دوباره بررسی کنه. func (s *OnboardingService) resubmitIfRejected(user *models.User) error { if !user.IsOnboarded || user.ApprovalStatus != models.ApprovalRejected { return nil } updates := map[string]interface{}{ "approval_status": models.ApprovalPending, "rejection_reason": nil, } if err := s.userRepo.UpdateFields(user, updates); err != nil { return err } user.ApprovalStatus = models.ApprovalPending user.RejectionReason = nil return nil } // ---------------- مرحله ۱: اطلاعات پایه ---------------- func (s *OnboardingService) SetBasicInfo(user *models.User, req dto.BasicInfoRequest) error { if err := s.canModifyStep(user, models.StepBasicInfo); err != nil { return err } birthDate, err := time.Parse("2006-01-02", req.BirthDate) if err != nil { return ErrInvalidBirthDate } gender := models.Gender(req.Gender) if err := s.userRepo.UpdateFields(user, map[string]interface{}{ "gender": gender, "first_name": req.FirstName, "birth_date": birthDate, }); err != nil { return err } return s.advanceIfCurrent(user, models.StepBasicInfo) } // ---------------- مرحله ۲: قد و وزن ---------------- func (s *OnboardingService) SetBodyInfo(user *models.User, req dto.BodyInfoRequest) error { if err := s.canModifyStep(user, models.StepBodyInfo); err != nil { return err } if err := s.userRepo.UpdateFields(user, map[string]interface{}{ "height_cm": req.HeightCM, "weight_kg": req.WeightKG, }); err != nil { return err } return s.advanceIfCurrent(user, models.StepBodyInfo) } // ---------------- مرحله ۳: سوالات پروفایل (دقیقاً ۳ سوال) ---------------- func (s *OnboardingService) GetAvailableQuestions() ([]models.ProfileQuestion, error) { return s.questionRepo.ListActive() } func (s *OnboardingService) SetProfileQuestions(user *models.User, req dto.ProfileQuestionsRequest) error { if err := s.canModifyStep(user, models.StepProfileQuestions); err != nil { return err } ids := make([]uint, 0, len(req.Answers)) seen := map[uint]bool{} for _, a := range req.Answers { if seen[a.QuestionID] { return ErrDuplicateQuestion } seen[a.QuestionID] = true ids = append(ids, a.QuestionID) } questions, err := s.questionRepo.FindActiveByIDs(ids) if err != nil { return err } if len(questions) != len(ids) { return ErrQuestionsInvalid } answers := make([]models.UserProfileAnswer, 0, len(req.Answers)) for _, a := range req.Answers { answers = append(answers, models.UserProfileAnswer{ QuestionID: a.QuestionID, Answer: a.Answer, }) } if err := s.questionRepo.ReplaceUserAnswers(user.ID, answers); err != nil { return err } return s.advanceIfCurrent(user, models.StepProfileQuestions) } // ---------------- مرحله ۴: تحصیلات (سطح تحصیلات + رشته) ---------------- func (s *OnboardingService) SetEducation(user *models.User, req dto.EducationRequest) error { if err := s.canModifyStep(user, models.StepEducation); err != nil { return err } if err := s.selectSingleOption(user.ID, models.CategoryEducationLevel, req.EducationLevelOptionID); err != nil { return err } if err := s.selectSingleOption(user.ID, models.CategoryFieldOfStudy, req.FieldOfStudyOptionID); err != nil { return err } return s.advanceIfCurrent(user, models.StepEducation) } func (s *OnboardingService) selectSingleOption(userID uint, key models.CategoryKey, optionID uint) error { return s.selectOptions(userID, key, []uint{optionID}) } // ---------------- مراحل مبتنی بر لیست چندانتخابی عمومی ---------------- // SetOccupations مرحله ۵: کارهایی که کاربر انجام می‌دهد (چندانتخابی، بدون تعداد ثابت) func (s *OnboardingService) SetOccupations(user *models.User, optionIDs []uint) error { if err := s.canModifyStep(user, models.StepOccupations); err != nil { return err } if err := s.selectOptions(user.ID, models.CategoryOccupation, optionIDs); err != nil { return err } return s.advanceIfCurrent(user, models.StepOccupations) } // SetRelationshipImportance مرحله ۶: دقیقاً ۳ مورد از چیزهای مهم در رابطه func (s *OnboardingService) SetRelationshipImportance(user *models.User, optionIDs []uint) error { if err := s.canModifyStep(user, models.StepRelationshipImportance); err != nil { return err } if err := s.selectOptions(user.ID, models.CategoryImportantInRelationship, optionIDs); err != nil { return err } return s.advanceIfCurrent(user, models.StepRelationshipImportance) } // SetInterests مرحله ۷: دقیقاً ۵ علاقه‌مندی func (s *OnboardingService) SetInterests(user *models.User, optionIDs []uint) error { if err := s.canModifyStep(user, models.StepInterests); err != nil { return err } if err := s.selectOptions(user.ID, models.CategoryInterest, optionIDs); err != nil { return err } return s.advanceIfCurrent(user, models.StepInterests) } // SetLookingFor مرحله ۸: دنبال چی می‌گردی func (s *OnboardingService) SetLookingFor(user *models.User, optionIDs []uint) error { if err := s.canModifyStep(user, models.StepLookingFor); err != nil { return err } if err := s.selectOptions(user.ID, models.CategoryLookingFor, optionIDs); err != nil { return err } return s.advanceIfCurrent(user, models.StepLookingFor) } // selectOptions منطق مشترک اعتبارسنجی + ذخیره‌ی انتخاب‌های یک دسته func (s *OnboardingService) selectOptions(userID uint, key models.CategoryKey, optionIDs []uint) error { cat, err := s.optionRepo.GetCategoryByKey(key) if err != nil { return err } count := len(optionIDs) if count < cat.MinSelect { return fmt.Errorf("%w: حداقل باید %d مورد انتخاب کنید", ErrSelectionCount, cat.MinSelect) } if cat.MaxSelect > 0 && count > cat.MaxSelect { return fmt.Errorf("%w: حداکثر می‌توانید %d مورد انتخاب کنید", ErrSelectionCount, cat.MaxSelect) } // جلوگیری از تکراری بودن id ها uniqueIDs := make([]uint, 0, len(optionIDs)) seen := map[uint]bool{} for _, id := range optionIDs { if !seen[id] { seen[id] = true uniqueIDs = append(uniqueIDs, id) } } validOptions, err := s.optionRepo.FindActiveOptionsByIDsInCategory(cat.ID, uniqueIDs) if err != nil { return err } if len(validOptions) != len(uniqueIDs) { return ErrOptionsNotFound } return s.optionRepo.ReplaceUserSelections(userID, cat.ID, uniqueIDs) } // GetActiveOptions لیست گزینه‌های فعال یک دسته را برمی‌گرداند (برای نمایش به کاربر در هر مرحله) func (s *OnboardingService) GetActiveOptions(key models.CategoryKey) ([]models.Option, error) { return s.optionRepo.ListActiveOptionsByCategoryKey(key) } // ---------------- وضعیت کلی آنبوردینگ ---------------- func (s *OnboardingService) GetStatus(user *models.User) dto.OnboardingStatusResponse { steps := make([]dto.StepView, 0, len(models.OnboardingStepOrder)) for _, s2 := range models.OnboardingStepOrder { done := user.IsOnboarded || user.CurrentStep > s2.Step steps = append(steps, dto.StepView{ Step: int(s2.Step), Key: s2.Key, Done: done, }) } currentKey := "completed" for _, s2 := range models.OnboardingStepOrder { if s2.Step == user.CurrentStep { currentKey = s2.Key break } } return dto.OnboardingStatusResponse{ CurrentStep: int(user.CurrentStep), CurrentStepKey: currentKey, IsOnboarded: user.IsOnboarded, ApprovalStatus: string(user.ApprovalStatus), RejectionReason: user.RejectionReason, Steps: steps, } }