Files
chat_room/internal/service/onboarding_service.go
T
mobiniqow 2823fb45f5 Initial commit: dating app backend (Go/Gin/GORM/Postgres)
Phone+OTP auth, staged onboarding, admin approval workflow, and a
full discovery/swipe/match/chat system with photo upload and
approximate distance (haversine) based on user location.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 23:23:47 +03:30

307 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
}
}