Skip to content

Commit 3764cb1

Browse files
fix: resolve calorie limit auto-update issue and add optimized goal achievement date calculation
1 parent 3ebc0e5 commit 3764cb1

9 files changed

Lines changed: 351 additions & 39 deletions

File tree

app.db

96 KB
Binary file not shown.

backend/database/models/user_goal_setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from sqlalchemy import Column, Integer, Float, Date, ForeignKey, DateTime
1+
from sqlalchemy import Column, Integer, Float, Date, ForeignKey, DateTime, String
22
from sqlalchemy.sql import func
33
from database.database import Base
44

@@ -9,6 +9,7 @@ class UserGoal(Base):
99

1010
target_weight = Column(Float, nullable=False)
1111
weekly_goal_kg = Column(Float, nullable=False)
12+
goal_type = Column(String(20), default="lose", nullable=False)
1213

1314
target_date = Column(Date, nullable=False)
1415

backend/goal_service/api.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,16 @@ def set_goal(
2929
profile,
3030
payload.target_weight,
3131
payload.weekly_goal_kg,
32-
target_calories=payload.target_calories # Use user input as primary source
32+
target_calories=payload.target_calories,
33+
goal_type=payload.goal_type
3334
)
3435

3536
if existing:
3637
# Update existing goal
3738
existing.target_weight = payload.target_weight
3839
existing.weekly_goal_kg = payload.weekly_goal_kg
39-
existing.daily_calories = payload.target_calories # Store user's target, not calculated
40+
existing.goal_type = payload.goal_type
41+
existing.daily_calories = payload.target_calories
4042
existing.protein_g = result["protein_g"]
4143
existing.carbs_g = result["carbs_g"]
4244
existing.fat_g = result["fat_g"]
@@ -50,7 +52,8 @@ def set_goal(
5052
user_id=current_user.id,
5153
target_weight=payload.target_weight,
5254
weekly_goal_kg=payload.weekly_goal_kg,
53-
daily_calories=payload.target_calories, # Store user's target
55+
goal_type=payload.goal_type,
56+
daily_calories=payload.target_calories,
5457
protein_g=result["protein_g"],
5558
carbs_g=result["carbs_g"],
5659
fat_g=result["fat_g"],
@@ -74,4 +77,4 @@ def get_my_goal(
7477
if not goal:
7578
raise HTTPException(404, "Goal not found")
7679

77-
return goal
80+
return goal

backend/goal_service/calculator.py

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"sedentary": 1.2,
66
"light": 1.375,
77
"moderate": 1.55,
8-
"active": 1.725
8+
"active": 1.725,
9+
"very_active": 1.9
910
}
1011

1112
def calculate_bmr(weight, height, age, gender):
@@ -15,28 +16,89 @@ def calculate_bmr(weight, height, age, gender):
1516
return 10*weight + 6.25*height - 5*age - 161
1617

1718

18-
def calculate_goal(profile, target_weight, weekly_goal_kg, target_calories=None):
19+
def calculate_adjusted_calories(bmr, activity_level, goal_type, weekly_rate):
1920
"""
20-
Calculate goal with macros based on target calories.
21+
Calculate TDEE with adjustment based on goal type and weekly rate.
22+
23+
Args:
24+
bmr: Basal Metabolic Rate
25+
activity_level: User's activity level string
26+
goal_type: "lose", "gain", or "maintain"
27+
weekly_rate: Weekly weight change rate (kg/week)
28+
29+
Returns:
30+
Adjusted daily calorie target (int)
31+
"""
32+
# Energy equivalent: 1 kg ≈ 7700 kcal
33+
base_tdee = bmr * ACTIVITY_FACTORS.get(activity_level, 1.2)
34+
35+
if goal_type == "maintain":
36+
return int(round(base_tdee))
37+
38+
# Daily calorie adjustment: (weeklyRate * 7700) / 7
39+
daily_adjustment = (weekly_rate * 7700) / 7
40+
41+
if goal_type == "lose":
42+
adjusted = base_tdee - daily_adjustment
43+
elif goal_type == "gain":
44+
adjusted = base_tdee + daily_adjustment
45+
else:
46+
adjusted = base_tdee
47+
48+
# Ensure minimum of 1000 kcal/day to prevent dangerously low intake
49+
return max(1000, int(round(adjusted)))
50+
51+
52+
def calculate_estimated_date(current_weight, target_weight, weekly_rate):
53+
"""
54+
Calculate estimated date to reach target weight.
55+
56+
Args:
57+
current_weight: Current weight in kg
58+
target_weight: Target weight in kg
59+
weekly_rate: Weekly weight change rate (kg/week)
60+
61+
Returns:
62+
Estimated completion date
63+
"""
64+
if weekly_rate == 0:
65+
return None
66+
67+
weight_difference = abs(current_weight - target_weight)
68+
weeks_required = math.ceil(weight_difference / weekly_rate)
69+
days_required = weeks_required * 7
70+
71+
return date.today() + timedelta(days=days_required)
72+
73+
74+
def calculate_goal(profile, target_weight, weekly_goal_kg, target_calories=None, goal_type="lose"):
75+
"""
76+
Calculate goal with macros based on target calories and goal type.
2177
2278
Args:
2379
profile: User profile with health metrics
2480
target_weight: Target weight in kg
25-
weekly_goal_kg: Weekly weight loss goal in kg
81+
weekly_goal_kg: Weekly weight loss/gain goal in kg
2682
target_calories: User's target daily calories (if None, calculated from profile)
83+
goal_type: "lose", "gain", or "maintain"
2784
"""
2885

29-
# If no target_calories provided, calculate from profile
86+
# Calculate BMR once
87+
bmr = calculate_bmr(
88+
profile.weight_kg,
89+
profile.height_cm,
90+
profile.age,
91+
profile.gender
92+
)
93+
94+
# If no target_calories provided, calculate from profile with adjustment
3095
if target_calories is None:
31-
bmr = calculate_bmr(
32-
profile.weight_kg,
33-
profile.height_cm,
34-
profile.age,
35-
profile.gender
96+
daily_calories = calculate_adjusted_calories(
97+
bmr,
98+
profile.activity_level,
99+
goal_type,
100+
weekly_goal_kg
36101
)
37-
tdee = bmr * ACTIVITY_FACTORS.get(profile.activity_level, 1.2)
38-
deficit = 500 if weekly_goal_kg == 0.5 else 1000
39-
daily_calories = int(tdee - deficit)
40102
else:
41103
# Use provided target_calories as source of truth
42104
daily_calories = target_calories
@@ -47,10 +109,10 @@ def calculate_goal(profile, target_weight, weekly_goal_kg, target_calories=None)
47109
carbs_g = int(daily_calories * 0.40 / 4)
48110
fat_g = int(daily_calories * 0.30 / 9)
49111

50-
weeks = abs(profile.weight_kg - target_weight) / weekly_goal_kg
51-
days = math.ceil(weeks * 7)
52-
53-
target_date = date.today() + timedelta(days=days)
112+
# Calculate estimated date
113+
target_date = calculate_estimated_date(profile.weight_kg, target_weight, weekly_goal_kg)
114+
if target_date is None:
115+
target_date = date.today()
54116

55117
return {
56118
"daily_calories": daily_calories,

backend/goal_service/schemas.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ class GoalCreate(BaseModel):
55
target_weight: float = Field(..., gt=0)
66
weekly_goal_kg: float = Field(..., description="0.5 or 1")
77
target_calories: int = Field(..., gt=0, description="Daily calorie target")
8+
goal_type: str = Field(default="lose", description="lose, gain, or maintain")
89

910

1011

@@ -16,8 +17,9 @@ class GoalResponse(BaseModel):
1617
carbs_g: int
1718
fat_g: int
1819
target_date: date
19-
target_weight: float # Use the actual database field name
20+
target_weight: float
2021
weekly_goal_kg: float
22+
goal_type: str = "lose"
2123

2224
@field_serializer('target_date')
2325
def serialize_target_date(self, value: date) -> str:

backend/profile_service/api.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from profile_service.schemas import ProfileCreate, ProfileResponse
55
from database.models.user_profile_setup import UserProfile
66
from auth_service.dependencies import get_current_user
7+
from goal_service.calculator import calculate_bmr, ACTIVITY_FACTORS
78

89
router = APIRouter(prefix="/profile", tags=["Profile"])
910

@@ -33,13 +34,29 @@ def setup_profile(
3334
db.add(profile)
3435

3536
db.commit()
36-
37-
if existing:
38-
db.refresh(existing)
39-
return existing
40-
else:
41-
db.refresh(profile)
42-
return profile
37+
38+
# Calculate recommended calories using Mifflin-St Jeor + activity multiplier
39+
# Use payload values (updated values) for calculation
40+
try:
41+
bmr = calculate_bmr(payload.weight_kg, payload.height_cm, payload.age, payload.gender)
42+
multiplier = ACTIVITY_FACTORS.get(payload.activity_level, 1.2)
43+
recommended = int(round(bmr * multiplier))
44+
# Ensure within reasonable validated bounds (frontend expects >=1000)
45+
recommended = max(1000, recommended)
46+
except Exception:
47+
recommended = 0
48+
49+
response_obj = {
50+
"user_id": current_user.id,
51+
"age": payload.age,
52+
"height_cm": payload.height_cm,
53+
"weight_kg": payload.weight_kg,
54+
"gender": payload.gender,
55+
"activity_level": payload.activity_level,
56+
"recommended_calories": recommended,
57+
}
58+
59+
return response_obj
4360

4461

4562
@router.get("/me", response_model=ProfileResponse)
@@ -53,4 +70,21 @@ def get_my_profile(
5370
if not profile:
5471
raise HTTPException(404, "Profile not found")
5572

56-
return profile
73+
# Compute recommended calories based on stored profile
74+
try:
75+
bmr = calculate_bmr(profile.weight_kg, profile.height_cm, profile.age, profile.gender)
76+
multiplier = ACTIVITY_FACTORS.get(profile.activity_level, 1.2)
77+
recommended = int(round(bmr * multiplier))
78+
recommended = max(1000, recommended)
79+
except Exception:
80+
recommended = 0
81+
82+
return {
83+
"user_id": profile.user_id,
84+
"age": profile.age,
85+
"height_cm": profile.height_cm,
86+
"weight_kg": profile.weight_kg,
87+
"gender": profile.gender,
88+
"activity_level": profile.activity_level,
89+
"recommended_calories": recommended,
90+
}

backend/profile_service/schemas.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ class ProfileCreate(BaseModel):
99

1010

1111
class ProfileResponse(ProfileCreate):
12-
user_id: int
12+
user_id: int
13+
recommended_calories: int = 0

frontend/src/lib/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ export interface GoalsData {
154154
fat_goal: number;
155155
weight_goal: number;
156156
weekly_goal_kg: number;
157+
goal_type?: string;
157158
}
158159

159160
export const goalsApi = {
@@ -164,6 +165,7 @@ export const goalsApi = {
164165
target_weight: data.weight_goal,
165166
weekly_goal_kg: data.weekly_goal_kg,
166167
target_calories: data.target_calories, // Calories are primary input
168+
goal_type: data.goal_type || "lose",
167169
});
168170
},
169171
me: () => {

0 commit comments

Comments
 (0)