Spaces:
Running
Running
| from typing import Optional, List, Dict, Any, Union | |
| import uuid | |
| from datetime import datetime, date | |
| from domain.runner.goal import Goal | |
| from domain.training.weekly_snapshot import WeeklySnapshot | |
| from domain.training.period_comparison import PeriodComparison | |
| from persistence.repositories.goal_repo import SqlGoalRepository | |
| from persistence.repositories.null_goal_repo import NullGoalRepository | |
| from observability import logger as obs_logger | |
| from observability import components as obs_components | |
| class GoalService: | |
| """Service for deterministic goal progress calculation and lifecycle management.""" | |
| def __init__(self, goal_repo: Union[SqlGoalRepository, NullGoalRepository]): | |
| self.goal_repo = goal_repo | |
| def create_goal(self, runner_id: uuid.UUID, goal_type: str, target_value: float, unit: str, target_date: Optional[datetime] = None) -> Goal: | |
| """Creates a new goal, archiving the existing active one.""" | |
| obs_logger.log_event( | |
| "info", | |
| "Step progress computed", | |
| component=obs_components.SERVICE, | |
| fields={"runner_id": str(runner_id), "type": goal_type} | |
| ) | |
| # Archive current active goal | |
| self.goal_repo.archive_active_goals(runner_id) | |
| obs_logger.log_event( | |
| "info", | |
| "goal_archived", | |
| component=obs_components.SERVICE, | |
| fields={"runner_id": str(runner_id)} | |
| ) | |
| new_goal = Goal( | |
| id=uuid.uuid4(), | |
| runner_id=runner_id, | |
| type=goal_type, | |
| target_value=target_value, | |
| unit=unit, | |
| target_date=target_date, | |
| status="active" | |
| ) | |
| self.goal_repo.create_goal(new_goal) | |
| obs_logger.log_event( | |
| "info", | |
| "goal_activated", | |
| component=obs_components.SERVICE, | |
| fields={"goal_id": str(new_goal.id)} | |
| ) | |
| return new_goal | |
| def archive_goal(self, goal_id: uuid.UUID) -> None: | |
| """Manually archives a goal.""" | |
| # This would require a get_goal_by_id in repo, but for now we work with active goals | |
| # Simplified for this refactor as per objectives | |
| pass | |
| def get_active_goal(self, runner_id: uuid.UUID) -> Optional[Goal]: | |
| """Retrieves the active goal for a runner.""" | |
| return self.goal_repo.get_active_goal(runner_id) | |
| def compute_goal_progress( | |
| self, | |
| goal: Goal, | |
| weekly_snapshot: Optional[WeeklySnapshot] = None, | |
| period_comparison: Optional[PeriodComparison] = None, | |
| ) -> Dict[str, Any]: | |
| """ | |
| Computes progress percentage and status for a goal. | |
| Rules: | |
| - Distance >= 100% -> On Track | |
| - 80-99% -> Slightly Behind | |
| - < 80% -> Behind | |
| """ | |
| progress_percentage = 0.0 | |
| status = "behind" | |
| if goal.type == "volume": | |
| if weekly_snapshot and goal.target_value > 0: | |
| progress_percentage = (weekly_snapshot.total_distance_km / goal.target_value) * 100 | |
| elif goal.type == "pace": | |
| if weekly_snapshot and weekly_snapshot.avg_pace_sec_per_km > 0: | |
| # Target Pace / Current Pace (lower pace is better) | |
| # Example: Target 300s/km, Current 330s/km -> 300/330 = 90.9% | |
| progress_percentage = ( | |
| goal.target_value / weekly_snapshot.avg_pace_sec_per_km | |
| ) * 100 | |
| elif goal.type == "race": | |
| # Simple V1: based on weekly volume increase (growth trend toward race date) | |
| if period_comparison: | |
| growth = getattr( | |
| period_comparison, | |
| "distance_delta_pct", | |
| getattr(period_comparison, "distance_delta_km", 0.0), | |
| ) | |
| if not isinstance(growth, (int, float)): | |
| progress_percentage = 0.0 | |
| elif growth >= 5.0: | |
| progress_percentage = 100.0 | |
| elif growth >= 0: | |
| progress_percentage = 80.0 + (growth * 4.0) # 0% -> 80%, 5% -> 100% | |
| else: | |
| progress_percentage = max(0.0, 80.0 + growth * 2.0) # -10% -> 60% | |
| elif weekly_snapshot: | |
| # No comparison? fallback to 50% if we have recent data | |
| progress_percentage = 50.0 | |
| # Clamp progress | |
| progress_percentage = min(100.0, max(0.0, progress_percentage)) | |
| if progress_percentage >= 100.0: | |
| status = "on_track" | |
| elif progress_percentage >= 80.0: | |
| status = "slightly_behind" | |
| else: | |
| status = "behind" | |
| return { | |
| "progress_percentage": round(progress_percentage, 1), | |
| "status": status, | |
| "goal_type": goal.type, | |
| "target_value": goal.target_value, | |
| "target_unit": goal.unit, | |
| } | |