package profilefieldoptions import ( "Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/ports" "context" "fmt" "regexp" "strings" ) var optionCodePattern = regexp.MustCompile(`^[A-Z][A-Z0-9_]*$`) type Service struct { store ports.ProfileFieldOptionStore } func NewService(store ports.ProfileFieldOptionStore) *Service { return &Service{store: store} } func normalizeOptionStatus(status *string) (string, error) { if status == nil || strings.TrimSpace(*status) == "" { return domain.ProfileFieldOptionStatusActive, nil } value := strings.ToUpper(strings.TrimSpace(*status)) switch value { case domain.ProfileFieldOptionStatusActive, domain.ProfileFieldOptionStatusInactive: return value, nil default: return "", fmt.Errorf("status must be one of %s, %s", domain.ProfileFieldOptionStatusActive, domain.ProfileFieldOptionStatusInactive) } } func normalizeOptionCode(code string) (string, error) { normalized := strings.ToUpper(strings.TrimSpace(code)) normalized = strings.ReplaceAll(normalized, " ", "_") normalized = strings.ReplaceAll(normalized, "-", "_") if !optionCodePattern.MatchString(normalized) { return "", domain.ErrInvalidOptionCode } return normalized, nil } var fieldKeyPattern = regexp.MustCompile(`^[a-z][a-z0-9_]*$`) func normalizeFieldKey(fieldKey string) (string, error) { key := strings.TrimSpace(strings.ToLower(fieldKey)) if !fieldKeyPattern.MatchString(key) { return "", domain.ErrInvalidFieldKey } return key, nil } func (s *Service) CreateProfileFieldOption(ctx context.Context, input domain.CreateProfileFieldOptionInput) (domain.ProfileFieldOption, error) { fieldKey, err := normalizeFieldKey(input.FieldKey) if err != nil { return domain.ProfileFieldOption{}, err } code, err := normalizeOptionCode(input.Code) if err != nil { return domain.ProfileFieldOption{}, err } label := strings.TrimSpace(input.Label) if label == "" { return domain.ProfileFieldOption{}, fmt.Errorf("label is required") } status, err := normalizeOptionStatus(input.Status) if err != nil { return domain.ProfileFieldOption{}, err } input.FieldKey = fieldKey input.Code = code input.Label = label input.Status = &status return s.store.CreateProfileFieldOption(ctx, input) } func (s *Service) UpdateProfileFieldOption(ctx context.Context, id int64, input domain.UpdateProfileFieldOptionInput) (domain.ProfileFieldOption, error) { if id <= 0 { return domain.ProfileFieldOption{}, fmt.Errorf("invalid profile field option id") } if input.Label != nil { trimmed := strings.TrimSpace(*input.Label) if trimmed == "" { return domain.ProfileFieldOption{}, fmt.Errorf("label cannot be empty") } input.Label = &trimmed } if input.Status != nil { status, err := normalizeOptionStatus(input.Status) if err != nil { return domain.ProfileFieldOption{}, err } input.Status = &status } return s.store.UpdateProfileFieldOption(ctx, id, input) } func (s *Service) GetProfileFieldOptionByID(ctx context.Context, id int64, includeInactive bool) (domain.ProfileFieldOption, error) { if id <= 0 { return domain.ProfileFieldOption{}, fmt.Errorf("invalid profile field option id") } return s.store.GetProfileFieldOptionByID(ctx, id, includeInactive) } func (s *Service) ListProfileFieldOptions(ctx context.Context, fieldKey *string, status *string, limit, offset int32) ([]domain.ProfileFieldOption, int64, error) { if fieldKey != nil { normalized, err := normalizeFieldKey(*fieldKey) if err != nil { return nil, 0, err } fieldKey = &normalized } if status != nil { normalized, err := normalizeOptionStatus(status) if err != nil { return nil, 0, err } status = &normalized } if limit <= 0 { limit = 50 } if limit > 500 { limit = 500 } if offset < 0 { offset = 0 } return s.store.ListProfileFieldOptions(ctx, fieldKey, status, limit, offset) } func (s *Service) ListActiveOptionsGrouped(ctx context.Context, fieldKey *string) (domain.ProfileFieldOptionsGrouped, error) { if fieldKey != nil { normalized, err := normalizeFieldKey(*fieldKey) if err != nil { return nil, err } fieldKey = &normalized } options, err := s.store.ListActiveProfileFieldOptions(ctx, fieldKey) if err != nil { return nil, err } grouped := make(domain.ProfileFieldOptionsGrouped) for _, opt := range options { grouped[opt.FieldKey] = append(grouped[opt.FieldKey], domain.ProfileFieldOptionItem{ Code: opt.Code, Label: opt.Label, }) } return grouped, nil } func (s *Service) ListFieldKeys(ctx context.Context, activeOnly bool) ([]string, error) { return s.store.ListDistinctFieldKeys(ctx, activeOnly) } func (s *Service) DeleteProfileFieldOption(ctx context.Context, id int64) error { if id <= 0 { return fmt.Errorf("invalid profile field option id") } return s.store.DeleteProfileFieldOption(ctx, id) } func (s *Service) ValidateUserProfileFieldValues(ctx context.Context, req domain.UpdateUserReq) error { checks := []struct { fieldKey string value string }{ {domain.ProfileFieldEducationLevel, req.EducationLevel}, {domain.ProfileFieldOccupation, req.Occupation}, {domain.ProfileFieldLearningGoal, req.LearningGoal}, {domain.ProfileFieldLanguageChallange, req.LanguageChallange}, {domain.ProfileFieldLanguageGoal, req.LanguageGoal}, {domain.ProfileFieldFavouriteTopic, req.FavouriteTopic}, } for _, check := range checks { value := strings.TrimSpace(check.value) if value == "" { continue } ok, err := s.store.IsActiveProfileFieldOption(ctx, check.fieldKey, value) if err != nil { return err } if !ok { return fmt.Errorf("invalid value for %s: %s", check.fieldKey, value) } } if req.AgeGroup != nil { age := strings.TrimSpace(string(*req.AgeGroup)) if age != "" { ok, err := s.store.IsActiveProfileFieldOption(ctx, domain.ProfileFieldAgeGroup, age) if err != nil { return err } if !ok { return fmt.Errorf("invalid value for %s: %s", domain.ProfileFieldAgeGroup, age) } } } return nil }