Kairo-Jinkendo/backend/services/maturity_practice.py
Lars 192a65a798
All checks were successful
Deploy Development / deploy (push) Successful in 1m1s
Test Suite / pytest-backend (push) Successful in 4m43s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 16s
fix(A1): enforce_single_active via roadmaps join (pytest)
roadmap_items hat kein initiative_id — Single-Active-Update join auf roadmaps. test_ap20e an Uebungssets angepasst.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 13:46:54 +02:00

194 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""A1 — Stage-bound practice exercises (Activity Set)."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any, Optional
from services.recurring import (
create_recurring_element,
list_recurring_for_initiative,
update_recurring_element,
)
# Starter exercise sets per stage title (Spagat reference path).
STAGE_PRACTICE_SETS: dict[str, list[tuple[str, str]]] = {
"Stufe 1 — Basis": [
("Vorbeuge-Dehnung", "Täglich 3×30 Sek. — Hüfte und Beinrückseite"),
("Hüftöffner (Schmetterling)", "Täglich sitzend, leichter Druck"),
],
"Stufe 2 — Aufbau": [
("Seitlicher Spagat — links", "Täglich halten, optional Messwert (Grad)"),
("Seitlicher Spagat — rechts", "Täglich halten, optional Messwert (Grad)"),
("Aktive Vorbeuge", "Täglich 3× kurz halten"),
],
"Stufe 3 — Ziel": [
("Voller Spagat", "Täglich Zielposition — Messwert dokumentieren"),
],
}
def get_active_maturity_stage(
*, tenant_id: str, initiative_id: str
) -> Optional[dict[str, Any]]:
from services.roadmap import list_roadmap_items_for_initiative
stages = [
item
for item in list_roadmap_items_for_initiative(
tenant_id=tenant_id, initiative_id=initiative_id
)
if item.get("item_type") == "maturity_stage" and item.get("status") == "active"
]
if not stages:
return None
stages.sort(key=lambda s: (s.get("sort_order", 0), s.get("title", "")))
return stages[0]
def enforce_single_active_maturity_stage(
cur,
*,
tenant_id: str,
initiative_id: str,
keep_active_id: str,
) -> None:
"""At most one maturity_stage may be active."""
cur.execute(
"""
UPDATE roadmap_items ri
SET status = 'planned', updated_at = NOW()
FROM roadmaps r
WHERE ri.roadmap_id = r.id
AND ri.tenant_id = %s
AND r.initiative_id = %s
AND ri.item_type = 'maturity_stage'
AND ri.status IN ('active', 'at_risk')
AND ri.id != %s
""",
(tenant_id, initiative_id, keep_active_id),
)
def list_practices_for_stage(
*,
tenant_id: str,
initiative_id: str,
stage_id: str,
) -> list[dict[str, Any]]:
items = list_recurring_for_initiative(
tenant_id=tenant_id, initiative_id=initiative_id
)
return [
item
for item in items
if item.get("roadmap_item_id") == stage_id
]
def pause_all_practices(
*,
tenant_id: str,
initiative_id: str,
user_id: Optional[str] = None,
) -> list[str]:
paused: list[str] = []
for item in list_recurring_for_initiative(
tenant_id=tenant_id, initiative_id=initiative_id
):
if item.get("status") == "active":
update_recurring_element(
tenant_id=tenant_id,
recurring_id=item["id"],
status="paused",
user_id=user_id,
)
paused.append(item["title"])
return paused
def seed_practices_for_stage(
*,
tenant_id: str,
initiative_id: str,
stage: dict[str, Any],
user_id: Optional[str] = None,
) -> list[dict[str, Any]]:
"""Idempotent: create missing exercises for a stage; activate existing."""
stage_id = stage["id"]
stage_title = stage.get("title") or ""
defs = STAGE_PRACTICE_SETS.get(stage_title)
if not defs:
defs = [
(
f"Übung — {stage_title}",
f"Tägliche Routine für {stage_title}",
)
]
existing = list_practices_for_stage(
tenant_id=tenant_id,
initiative_id=initiative_id,
stage_id=stage_id,
)
existing_by_title = {item["title"]: item for item in existing}
created: list[dict[str, Any]] = []
now = datetime.now(timezone.utc)
for title, description in defs:
if title in existing_by_title:
item = existing_by_title[title]
if item.get("status") != "active":
updated = update_recurring_element(
tenant_id=tenant_id,
recurring_id=item["id"],
status="active",
next_due_at=now,
user_id=user_id,
)
if updated:
created.append(updated)
else:
created.append(item)
continue
row = create_recurring_element(
tenant_id=tenant_id,
initiative_id=initiative_id,
title=title,
description=description,
status="active",
interval_days=1,
next_due_at=now,
roadmap_item_id=stage_id,
user_id=user_id,
)
created.append(row)
return created
def activate_stage_practice_set(
*,
tenant_id: str,
initiative_id: str,
stage: dict[str, Any],
user_id: Optional[str] = None,
) -> dict[str, Any]:
paused = pause_all_practices(
tenant_id=tenant_id, initiative_id=initiative_id, user_id=user_id
)
exercises = seed_practices_for_stage(
tenant_id=tenant_id,
initiative_id=initiative_id,
stage=stage,
user_id=user_id,
)
return {
"stage_id": stage["id"],
"stage_title": stage.get("title"),
"paused_practices": paused,
"active_practices": [e["title"] for e in exercises],
"practice_ids": [e["id"] for e in exercises],
}