feat: replace age integer with date_of_birth across backend and client

Store date_of_birth (DATE) instead of a static age integer so that age
is always computed dynamically from the stored date of birth.

- Migration 011: adds date_of_birth, backfills from age, drops age
- AgeFromDOB helper computes current age from YYYY-MM-DD string
- User model, repository SQL, and service validation updated
- Flutter: User.age becomes a computed getter; profile edit screen
  uses a date picker bounded to [today-120y, today-10y]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-03-09 23:37:58 +02:00
parent 765346d4e4
commit c9ddb708b1
13 changed files with 202 additions and 73 deletions

View File

@@ -45,7 +45,7 @@ func (r *Repository) UpsertByFirebaseUID(ctx context.Context, uid, email, name,
avatar_url = COALESCE(EXCLUDED.avatar_url, users.avatar_url),
updated_at = now()
RETURNING id, firebase_uid, email, name, avatar_url,
height_cm, weight_kg, age, gender, activity, goal, daily_calories,
height_cm, weight_kg, date_of_birth, gender, activity, goal, daily_calories,
plan, preferences, created_at, updated_at`
return r.scanUser(r.pool.QueryRow(ctx, query, uid, email, name, avatarPtr))
@@ -54,7 +54,7 @@ func (r *Repository) UpsertByFirebaseUID(ctx context.Context, uid, email, name,
func (r *Repository) GetByID(ctx context.Context, id string) (*User, error) {
query := `
SELECT id, firebase_uid, email, name, avatar_url,
height_cm, weight_kg, age, gender, activity, goal, daily_calories,
height_cm, weight_kg, date_of_birth, gender, activity, goal, daily_calories,
plan, preferences, created_at, updated_at
FROM users WHERE id = $1`
@@ -91,9 +91,9 @@ func buildUpdateQuery(id string, req UpdateProfileRequest) (string, []interface{
args = append(args, *req.WeightKG)
argIdx++
}
if req.Age != nil {
setClauses = append(setClauses, fmt.Sprintf("age = $%d", argIdx))
args = append(args, *req.Age)
if req.DateOfBirth != nil {
setClauses = append(setClauses, fmt.Sprintf("date_of_birth = $%d", argIdx))
args = append(args, *req.DateOfBirth)
argIdx++
}
if req.Gender != nil {
@@ -132,7 +132,7 @@ func buildUpdateQuery(id string, req UpdateProfileRequest) (string, []interface{
UPDATE users SET %s
WHERE id = $%d
RETURNING id, firebase_uid, email, name, avatar_url,
height_cm, weight_kg, age, gender, activity, goal, daily_calories,
height_cm, weight_kg, date_of_birth, gender, activity, goal, daily_calories,
plan, preferences, created_at, updated_at`,
strings.Join(setClauses, ", "), argIdx)
args = append(args, id)
@@ -184,7 +184,7 @@ func (r *Repository) SetRefreshToken(ctx context.Context, id, token string, expi
func (r *Repository) FindByRefreshToken(ctx context.Context, token string) (*User, error) {
query := `
SELECT id, firebase_uid, email, name, avatar_url,
height_cm, weight_kg, age, gender, activity, goal, daily_calories,
height_cm, weight_kg, date_of_birth, gender, activity, goal, daily_calories,
plan, preferences, created_at, updated_at
FROM users
WHERE refresh_token = $1 AND token_expires_at > now()`
@@ -205,14 +205,19 @@ type scannable interface {
func (r *Repository) scanUser(row scannable) (*User, error) {
var u User
var prefs []byte
var dob *time.Time
err := row.Scan(
&u.ID, &u.FirebaseUID, &u.Email, &u.Name, &u.AvatarURL,
&u.HeightCM, &u.WeightKG, &u.Age, &u.Gender, &u.Activity, &u.Goal, &u.DailyCalories,
&u.HeightCM, &u.WeightKG, &dob, &u.Gender, &u.Activity, &u.Goal, &u.DailyCalories,
&u.Plan, &prefs, &u.CreatedAt, &u.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("scan user: %w", err)
}
if dob != nil {
s := dob.Format("2006-01-02")
u.DateOfBirth = &s
}
u.Preferences = json.RawMessage(prefs)
return &u, nil
}