chore: merge backend repo history into monorepo at backend/
git-subtree-dir: backend git-subtree-mainline:dc11190527git-subtree-split:acae976622
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user