package user import ( "context" "fmt" "time" ) type Service struct { repo UserRepository } func NewService(repo UserRepository) *Service { return &Service{repo: repo} } func (s *Service) GetProfile(ctx context.Context, userID string) (*User, error) { return s.repo.GetByID(ctx, userID) } func (s *Service) UpdateProfile(ctx context.Context, userID string, req UpdateProfileRequest) (*User, error) { if err := validateProfileRequest(req); err != nil { return nil, err } if !req.HasBodyParams() { updated, err := s.repo.Update(ctx, userID, req) if err != nil { return nil, fmt.Errorf("update profile: %w", err) } return updated, nil } // Need to update profile + recalculate calories in a single transaction. // First, get current user to merge with incoming fields for calorie calculation. current, err := s.repo.GetByID(ctx, userID) if err != nil { return nil, fmt.Errorf("get user: %w", err) } // Merge current values with request to compute calories height := current.HeightCM if req.HeightCM != nil { height = req.HeightCM } weight := current.WeightKG if req.WeightKG != nil { weight = req.WeightKG } dob := current.DateOfBirth if req.DateOfBirth != nil { dob = req.DateOfBirth } age := AgeFromDOB(dob) gender := current.Gender if req.Gender != nil { gender = req.Gender } activity := current.Activity if req.Activity != nil { activity = req.Activity } goal := current.Goal if req.Goal != nil { goal = req.Goal } calories := CalculateDailyCalories(height, weight, age, gender, activity, goal) var calReq *UpdateProfileRequest if calories != nil { calReq = &UpdateProfileRequest{DailyCalories: calories} } updated, err := s.repo.UpdateInTx(ctx, userID, req, calReq) if err != nil { return nil, fmt.Errorf("update profile: %w", err) } return updated, nil } func validateProfileRequest(req UpdateProfileRequest) error { if req.HeightCM != nil { if *req.HeightCM < 100 || *req.HeightCM > 250 { return fmt.Errorf("height_cm must be between 100 and 250") } } if req.WeightKG != nil { if *req.WeightKG < 30 || *req.WeightKG > 300 { return fmt.Errorf("weight_kg must be between 30 and 300") } } if req.DateOfBirth != nil { t, err := time.Parse("2006-01-02", *req.DateOfBirth) if err != nil { return fmt.Errorf("date_of_birth must be in YYYY-MM-DD format") } if t.After(time.Now()) { return fmt.Errorf("date_of_birth cannot be in the future") } age := AgeFromDOB(req.DateOfBirth) if *age < 10 || *age > 120 { return fmt.Errorf("date_of_birth must yield an age between 10 and 120") } } if req.Gender != nil { if *req.Gender != "male" && *req.Gender != "female" { return fmt.Errorf("gender must be 'male' or 'female'") } } if req.Activity != nil { switch *req.Activity { case "low", "moderate", "high": default: return fmt.Errorf("activity must be 'low', 'moderate', or 'high'") } } if req.Goal != nil { switch *req.Goal { case "lose", "maintain", "gain": default: return fmt.Errorf("goal must be 'lose', 'maintain', or 'gain'") } } return nil }