35f5084005
git-subtree-dir: backend git-subtree-mainline:dc11190527git-subtree-split:acae976622
329 lines
11 KiB
Go
329 lines
11 KiB
Go
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("هر سوال فقط یکبار میتواند انتخاب شود")
|
||
ErrNoPhotos = errors.New("برای ادامه حداقل باید یک عکس اضافه کنی")
|
||
)
|
||
|
||
type OnboardingService struct {
|
||
userRepo *repository.UserRepository
|
||
optionRepo *repository.OptionRepository
|
||
questionRepo *repository.QuestionRepository
|
||
photoRepo *repository.PhotoRepository
|
||
}
|
||
|
||
func NewOnboardingService(
|
||
userRepo *repository.UserRepository,
|
||
optionRepo *repository.OptionRepository,
|
||
questionRepo *repository.QuestionRepository,
|
||
photoRepo *repository.PhotoRepository,
|
||
) *OnboardingService {
|
||
return &OnboardingService{
|
||
userRepo: userRepo,
|
||
optionRepo: optionRepo,
|
||
questionRepo: questionRepo,
|
||
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)
|
||
}
|
||
|
||
// ---------------- مرحله ۲: عکسها (حداقل ۱ عکس) ----------------
|
||
|
||
// 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,
|
||
}
|
||
}
|