Files
chat_room/backend/internal/service/onboarding_service.go
T

329 lines
11 KiB
Go
Raw Normal View History

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("هر سوال فقط یک‌بار می‌تواند انتخاب شود")
2026-09-16 09:21:50 +03:30
ErrNoPhotos = errors.New("برای ادامه حداقل باید یک عکس اضافه کنی")
)
type OnboardingService struct {
userRepo *repository.UserRepository
optionRepo *repository.OptionRepository
questionRepo *repository.QuestionRepository
2026-09-16 09:21:50 +03:30
photoRepo *repository.PhotoRepository
}
func NewOnboardingService(
userRepo *repository.UserRepository,
optionRepo *repository.OptionRepository,
questionRepo *repository.QuestionRepository,
2026-09-16 09:21:50 +03:30
photoRepo *repository.PhotoRepository,
) *OnboardingService {
return &OnboardingService{
userRepo: userRepo,
optionRepo: optionRepo,
questionRepo: questionRepo,
2026-09-16 09:21:50 +03:30
photoRepo: photoRepo,
}
}
// 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)
}
2026-09-16 09:21:50 +03:30
// ---------------- مرحله ۲: عکس‌ها (حداقل ۱ عکس) ----------------
// ConfirmPhotos عکس واقعی رو از طریق endpoint عمومی /profile/photos آپلود می‌کنیم؛
// این متد فقط تایید می‌کنه که حداقل یک عکس هست و مرحله رو جلو می‌بره.
func (s *OnboardingService) ConfirmPhotos(user *models.User) error {
if err := s.canModifyStep(user, models.StepPhotos); err != nil {
return err
}
count, err := s.photoRepo.CountByUser(user.ID)
if err != nil {
return err
}
if count < 1 {
return ErrNoPhotos
}
return s.advanceIfCurrent(user, models.StepPhotos)
}
// ---------------- مرحله ۳: قد و وزن ----------------
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,
}
}