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:
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"},
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user