""" core/routines.py - Shared business logic for routines This module contains reusable functions for routine operations that can be called from API routes, bot commands, or scheduler. """ import uuid from datetime import datetime, date import core.postgres as postgres import core.tz as tz def start_session(routine_id, user_uuid): """ Create and start a new routine session. Returns the session object or None if routine not found. """ routine = postgres.select_one("routines", {"id": routine_id, "user_uuid": user_uuid}) if not routine: return None active_session = postgres.select_one( "routine_sessions", {"user_uuid": user_uuid, "status": "active"} ) if active_session: return {"error": "already_active", "session_id": active_session["id"]} steps = postgres.select( "routine_steps", {"routine_id": routine_id}, order_by="position" ) if not steps: return {"error": "no_steps"} session = { "id": str(uuid.uuid4()), "routine_id": routine_id, "user_uuid": user_uuid, "status": "active", "current_step_index": 0, } result = postgres.insert("routine_sessions", session) return {"session": result, "current_step": steps[0]} def pause_session(session_id, user_uuid): """Pause an active session.""" session = postgres.select_one( "routine_sessions", {"id": session_id, "user_uuid": user_uuid} ) if not session: return None if session.get("status") != "active": return {"error": "not_active"} result = postgres.update( "routine_sessions", {"status": "paused", "paused_at": datetime.now().isoformat()}, {"id": session_id} ) return result def resume_session(session_id, user_uuid): """Resume a paused session.""" session = postgres.select_one( "routine_sessions", {"id": session_id, "user_uuid": user_uuid} ) if not session: return None if session.get("status") != "paused": return {"error": "not_paused"} result = postgres.update( "routine_sessions", {"status": "active", "paused_at": None}, {"id": session_id} ) return result def abort_session(session_id, user_uuid, reason=None): """Abort a session with optional reason.""" session = postgres.select_one( "routine_sessions", {"id": session_id, "user_uuid": user_uuid} ) if not session: return None result = postgres.update( "routine_sessions", { "status": "aborted", "abort_reason": reason or "Aborted by user", "completed_at": datetime.now().isoformat() }, {"id": session_id} ) return result def complete_session(session_id, user_uuid): """Mark a session as completed and update streak.""" session = postgres.select_one( "routine_sessions", {"id": session_id, "user_uuid": user_uuid} ) if not session: return None completed_at = datetime.now() result = postgres.update( "routine_sessions", {"status": "completed", "completed_at": completed_at.isoformat()}, {"id": session_id} ) _update_streak(user_uuid, session["routine_id"]) return result def clone_template(template_id, user_uuid): """Clone a template to user's routines.""" template = postgres.select_one("routine_templates", {"id": template_id}) if not template: return None template_steps = postgres.select( "routine_template_steps", {"template_id": template_id}, order_by="position" ) new_routine = { "id": str(uuid.uuid4()), "user_uuid": user_uuid, "name": template["name"], "description": template.get("description"), "icon": template.get("icon"), } routine = postgres.insert("routines", new_routine) for step in template_steps: new_step = { "id": str(uuid.uuid4()), "routine_id": routine["id"], "name": step["name"], "instructions": step.get("instructions"), "step_type": step.get("step_type", "generic"), "duration_minutes": step.get("duration_minutes"), "media_url": step.get("media_url"), "position": step["position"], } postgres.insert("routine_steps", new_step) return routine STREAK_MILESTONES = {3, 7, 14, 21, 30, 60, 90, 100, 365} def _update_streak(user_uuid, routine_id): """Update streak after completing a session. Resets if day was missed. Returns the updated streak dict with optional 'milestone' key.""" today = tz.user_today() streak = postgres.select_one( "routine_streaks", {"user_uuid": user_uuid, "routine_id": routine_id} ) if not streak: new_streak_val = 1 new_streak = { "id": str(uuid.uuid4()), "user_uuid": user_uuid, "routine_id": routine_id, "current_streak": 1, "longest_streak": 1, "last_completed_date": today.isoformat(), } result = postgres.insert("routine_streaks", new_streak) if new_streak_val in STREAK_MILESTONES: result["milestone"] = new_streak_val return result last_completed = streak.get("last_completed_date") if last_completed: if isinstance(last_completed, str): last_completed = date.fromisoformat(last_completed) days_diff = (today - last_completed).days if days_diff == 0: return streak elif days_diff == 1: new_streak_val = streak["current_streak"] + 1 else: new_streak_val = 1 else: new_streak_val = 1 longest = max(streak["longest_streak"], new_streak_val) postgres.update( "routine_streaks", { "current_streak": new_streak_val, "longest_streak": longest, "last_completed_date": today.isoformat(), }, {"id": streak["id"]} ) result = {**streak, "current_streak": new_streak_val, "longest_streak": longest} if new_streak_val in STREAK_MILESTONES: result["milestone"] = new_streak_val return result def calculate_streak(user_uuid, routine_id): """Get current streak for a routine.""" streak = postgres.select_one( "routine_streaks", {"user_uuid": user_uuid, "routine_id": routine_id} ) return streak def get_active_session(user_uuid): """Get user's currently active or paused session.""" session = postgres.select_one( "routine_sessions", {"user_uuid": user_uuid, "status": "active"} ) if not session: session = postgres.select_one( "routine_sessions", {"user_uuid": user_uuid, "status": "paused"} ) return session