Add photo upload as a real onboarding step

Photos are now step 2 of onboarding (right after basic info) instead
of only being manageable from the profile page later. Uploading still
goes through the existing /profile/photos endpoint; the new
POST /onboarding/photos just confirms at least one photo exists and
advances the step counter. Renumbered the OnboardingStep enum
accordingly (9 steps total now).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 09:21:50 +03:30
parent fda1b130ff
commit 15583009e0
6 changed files with 54 additions and 11 deletions
+14 -9
View File
@@ -78,23 +78,28 @@ func (s *KavenegarOTPSender) Send(phone, otp string) bool {
## فرآیند آنبوردینگ ## فرآیند آنبوردینگ
۸ مرحله‌ی ثابت که باید به‌ترتیب طی بشن (نمی‌شه از روی مرحله پرید، ولی مراحل ۹ مرحله‌ی ثابت که باید به‌ترتیب طی بشن (نمی‌شه از روی مرحله پرید، ولی مراحل
قبلی هر وقت بخوای قابل ویرایش‌اند؛ بعد از تکمیل کامل هم همه‌ی مراحل آزادانه قبلی هر وقت بخوای قابل ویرایش‌اند؛ بعد از تکمیل کامل هم همه‌ی مراحل آزادانه
قابل ویرایش می‌شن): قابل ویرایش می‌شن — حتی توی همون بازه‌ای که کاربر منتظر تایید ادمینه):
| # | مرحله | Endpoint | | # | مرحله | Endpoint |
|---|-------|----------| |---|-------|----------|
| ۱ | جنسیت، اسم، تاریخ تولد | `POST /onboarding/basic-info` | | ۱ | جنسیت، اسم، تاریخ تولد | `POST /onboarding/basic-info` |
| ۲ | قد و وزن | `POST /onboarding/body-info` | | ۲ | عکس‌ها (حداقل ۱ عکس؛ خودِ آپلود از `/profile/photos`) | `POST /onboarding/photos` |
| ۳ | ۳ سوال از پیش‌تعریف‌شده + پاسخ | `POST /onboarding/profile-questions` | | ۳ | قد و وزن | `POST /onboarding/body-info` |
| ۴ | سطح تحصیلات + رشته | `POST /onboarding/education` | | ۴ | ۳ سوال از پیش‌تعریف‌شده + پاسخ | `POST /onboarding/profile-questions` |
| ۵ | کارهایی که انجام می‌دهد (چندتایی) | `POST /onboarding/occupations` | | ۵ | سطح تحصیلات + رشته | `POST /onboarding/education` |
| ۶ | ۳ چیز مهم در رابطه | `POST /onboarding/relationship-importance` | | ۶ | کارهایی که انجام می‌دهد (چندتایی) | `POST /onboarding/occupations` |
| ۷ | ۵ علاقه‌مندی | `POST /onboarding/interests` | | ۷ | ۳ چیز مهم در رابطه | `POST /onboarding/relationship-importance` |
| ۸ | دنبال چی می‌گردی | `POST /onboarding/looking-for` | | ۸ | ۵ علاقه‌مندی | `POST /onboarding/interests` |
| ۹ | دنبال چی می‌گردی | `POST /onboarding/looking-for` |
وضعیت فعلی: `GET /onboarding/status` وضعیت فعلی: `GET /onboarding/status`
اگه کاربر قبلاً رد شده باشه و بعد از onboarded شدن هرکدوم از مراحل رو دوباره
بزنه (حتی مرحله‌ی عکس‌ها)، `approval_status` خودکار به `pending` برمی‌گرده تا
ادمین دوباره بررسی کنه.
### بعد از تکمیل آنبوردینگ: تایید ادمین ### بعد از تکمیل آنبوردینگ: تایید ادمین
به محض تکمیل مرحله‌ی ۸، `approval_status` کاربر روی `pending` قرار می‌گیرد. به محض تکمیل مرحله‌ی ۸، `approval_status` کاربر روی `pending` قرار می‌گیرد.
+1 -1
View File
@@ -60,7 +60,7 @@ func main() {
userRepo, otpSender, jwtManager, userRepo, otpSender, jwtManager,
cfg.OTPExpirySeconds, cfg.OTPLength, cfg.OTPHashSecret, cfg.OTPExpirySeconds, cfg.OTPLength, cfg.OTPHashSecret,
) )
onboardingService := service.NewOnboardingService(userRepo, optionRepo, questionRepo) onboardingService := service.NewOnboardingService(userRepo, optionRepo, questionRepo, photoRepo)
adminService := service.NewAdminService(optionRepo, questionRepo, userRepo) adminService := service.NewAdminService(optionRepo, questionRepo, userRepo)
profileService := service.NewProfileService(photoRepo, userRepo, photoStorage) profileService := service.NewProfileService(photoRepo, userRepo, photoStorage)
discoveryService := service.NewDiscoveryService(userRepo, photoRepo, swipeRepo, matchRepo, optionRepo, questionRepo) discoveryService := service.NewDiscoveryService(userRepo, photoRepo, swipeRepo, matchRepo, optionRepo, questionRepo)
+13
View File
@@ -66,6 +66,19 @@ func (h *OnboardingHandler) BasicInfo(c *gin.Context) {
utils.Success(c, http.StatusOK, "اطلاعات پایه ثبت شد", h.onboardingService.GetStatus(user)) utils.Success(c, http.StatusOK, "اطلاعات پایه ثبت شد", h.onboardingService.GetStatus(user))
} }
// Photos POST /onboarding/photos — تایید مرحله‌ی عکس‌ها (خودِ آپلود از /profile/photos انجام می‌شه)
func (h *OnboardingHandler) Photos(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
if err := h.onboardingService.ConfirmPhotos(user); err != nil {
h.handleStepError(c, err)
return
}
utils.Success(c, http.StatusOK, "عکس‌ها ثبت شد", h.onboardingService.GetStatus(user))
}
// BodyInfo POST /onboarding/body-info // BodyInfo POST /onboarding/body-info
func (h *OnboardingHandler) BodyInfo(c *gin.Context) { func (h *OnboardingHandler) BodyInfo(c *gin.Context) {
user, ok := currentUser(c) user, ok := currentUser(c)
+2
View File
@@ -26,6 +26,7 @@ type OnboardingStep int
const ( const (
StepBasicInfo OnboardingStep = iota + 1 StepBasicInfo OnboardingStep = iota + 1
StepPhotos
StepBodyInfo StepBodyInfo
StepProfileQuestions StepProfileQuestions
StepEducation StepEducation
@@ -58,6 +59,7 @@ var OnboardingStepOrder = []struct {
Key string Key string
}{ }{
{StepBasicInfo, "basic_info"}, {StepBasicInfo, "basic_info"},
{StepPhotos, "photos"},
{StepBodyInfo, "body_info"}, {StepBodyInfo, "body_info"},
{StepProfileQuestions, "profile_questions"}, {StepProfileQuestions, "profile_questions"},
{StepEducation, "education"}, {StepEducation, "education"},
+1
View File
@@ -59,6 +59,7 @@ func Setup(
{ {
onboarding.GET("/status", h.Onboarding.Status) onboarding.GET("/status", h.Onboarding.Status)
onboarding.POST("/basic-info", h.Onboarding.BasicInfo) onboarding.POST("/basic-info", h.Onboarding.BasicInfo)
onboarding.POST("/photos", h.Onboarding.Photos)
onboarding.POST("/body-info", h.Onboarding.BodyInfo) onboarding.POST("/body-info", h.Onboarding.BodyInfo)
onboarding.GET("/profile-questions", h.Onboarding.AvailableQuestions) onboarding.GET("/profile-questions", h.Onboarding.AvailableQuestions)
onboarding.POST("/profile-questions", h.Onboarding.SetProfileQuestions) onboarding.POST("/profile-questions", h.Onboarding.SetProfileQuestions)
+23 -1
View File
@@ -17,23 +17,27 @@ var (
ErrSelectionCount = errors.New("تعداد گزینه‌های انتخابی مجاز نیست") ErrSelectionCount = errors.New("تعداد گزینه‌های انتخابی مجاز نیست")
ErrQuestionsInvalid = errors.New("سوالات انتخابی معتبر نیستند") ErrQuestionsInvalid = errors.New("سوالات انتخابی معتبر نیستند")
ErrDuplicateQuestion = errors.New("هر سوال فقط یک‌بار می‌تواند انتخاب شود") ErrDuplicateQuestion = errors.New("هر سوال فقط یک‌بار می‌تواند انتخاب شود")
ErrNoPhotos = errors.New("برای ادامه حداقل باید یک عکس اضافه کنی")
) )
type OnboardingService struct { type OnboardingService struct {
userRepo *repository.UserRepository userRepo *repository.UserRepository
optionRepo *repository.OptionRepository optionRepo *repository.OptionRepository
questionRepo *repository.QuestionRepository questionRepo *repository.QuestionRepository
photoRepo *repository.PhotoRepository
} }
func NewOnboardingService( func NewOnboardingService(
userRepo *repository.UserRepository, userRepo *repository.UserRepository,
optionRepo *repository.OptionRepository, optionRepo *repository.OptionRepository,
questionRepo *repository.QuestionRepository, questionRepo *repository.QuestionRepository,
photoRepo *repository.PhotoRepository,
) *OnboardingService { ) *OnboardingService {
return &OnboardingService{ return &OnboardingService{
userRepo: userRepo, userRepo: userRepo,
optionRepo: optionRepo, optionRepo: optionRepo,
questionRepo: questionRepo, questionRepo: questionRepo,
photoRepo: photoRepo,
} }
} }
@@ -109,7 +113,25 @@ func (s *OnboardingService) SetBasicInfo(user *models.User, req dto.BasicInfoReq
return s.advanceIfCurrent(user, models.StepBasicInfo) 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 { func (s *OnboardingService) SetBodyInfo(user *models.User, req dto.BodyInfoRequest) error {
if err := s.canModifyStep(user, models.StepBodyInfo); err != nil { if err := s.canModifyStep(user, models.StepBodyInfo); err != nil {