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 |
|---|-------|----------|
| ۱ | جنسیت، اسم، تاریخ تولد | `POST /onboarding/basic-info` |
| ۲ | قد و وزن | `POST /onboarding/body-info` |
| ۳ | ۳ سوال از پیش‌تعریف‌شده + پاسخ | `POST /onboarding/profile-questions` |
| ۴ | سطح تحصیلات + رشته | `POST /onboarding/education` |
| ۵ | کارهایی که انجام می‌دهد (چندتایی) | `POST /onboarding/occupations` |
| ۶ | ۳ چیز مهم در رابطه | `POST /onboarding/relationship-importance` |
| ۷ | ۵ علاقه‌مندی | `POST /onboarding/interests` |
| ۸ | دنبال چی می‌گردی | `POST /onboarding/looking-for` |
| ۲ | عکس‌ها (حداقل ۱ عکس؛ خودِ آپلود از `/profile/photos`) | `POST /onboarding/photos` |
| ۳ | قد و وزن | `POST /onboarding/body-info` |
| ۴ | ۳ سوال از پیش‌تعریف‌شده + پاسخ | `POST /onboarding/profile-questions` |
| ۵ | سطح تحصیلات + رشته | `POST /onboarding/education` |
| ۶ | کارهایی که انجام می‌دهد (چندتایی) | `POST /onboarding/occupations` |
| ۷ | ۳ چیز مهم در رابطه | `POST /onboarding/relationship-importance` |
| ۸ | ۵ علاقه‌مندی | `POST /onboarding/interests` |
| ۹ | دنبال چی می‌گردی | `POST /onboarding/looking-for` |
وضعیت فعلی: `GET /onboarding/status`
اگه کاربر قبلاً رد شده باشه و بعد از onboarded شدن هرکدوم از مراحل رو دوباره
بزنه (حتی مرحله‌ی عکس‌ها)، `approval_status` خودکار به `pending` برمی‌گرده تا
ادمین دوباره بررسی کنه.
### بعد از تکمیل آنبوردینگ: تایید ادمین
به محض تکمیل مرحله‌ی ۸، `approval_status` کاربر روی `pending` قرار می‌گیرد.
+1 -1
View File
@@ -60,7 +60,7 @@ func main() {
userRepo, otpSender, jwtManager,
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)
profileService := service.NewProfileService(photoRepo, userRepo, photoStorage)
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))
}
// 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
func (h *OnboardingHandler) BodyInfo(c *gin.Context) {
user, ok := currentUser(c)
+2
View File
@@ -26,6 +26,7 @@ type OnboardingStep int
const (
StepBasicInfo OnboardingStep = iota + 1
StepPhotos
StepBodyInfo
StepProfileQuestions
StepEducation
@@ -58,6 +59,7 @@ var OnboardingStepOrder = []struct {
Key string
}{
{StepBasicInfo, "basic_info"},
{StepPhotos, "photos"},
{StepBodyInfo, "body_info"},
{StepProfileQuestions, "profile_questions"},
{StepEducation, "education"},
+1
View File
@@ -59,6 +59,7 @@ func Setup(
{
onboarding.GET("/status", h.Onboarding.Status)
onboarding.POST("/basic-info", h.Onboarding.BasicInfo)
onboarding.POST("/photos", h.Onboarding.Photos)
onboarding.POST("/body-info", h.Onboarding.BodyInfo)
onboarding.GET("/profile-questions", h.Onboarding.AvailableQuestions)
onboarding.POST("/profile-questions", h.Onboarding.SetProfileQuestions)
+23 -1
View File
@@ -17,23 +17,27 @@ var (
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,
}
}
@@ -109,7 +113,25 @@ func (s *OnboardingService) SetBasicInfo(user *models.User, req dto.BasicInfoReq
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 {