"""Общий путь закрытия задачи (ТЗ 3.5, 3.13): спавн регулярной + награды.
Один и тот же код используется HTTP-API и MCP-инструментами: закрытие задачи
агентом должно давать ровно те же XP/монеты/растение, что и закрытие из UI.
Идемпотентность: XP — один раз на задачу (grant_task_xp), спавн регулярной —
один раз на цепочку (метка spawned_at). Коммит делает вызывающий код.
"""
from dataclasses import dataclass
from sqlalchemy.orm import Session
from app.models import CoinEvent, Task, XpEvent, utcnow
from app.services import xp as xp_service
from app.services.garden import coins_for_xp, level_bonus_coins
from app.services.recurrence import spawn_next
@dataclass
class CloseOutcome:
"""Результат закрытия: что начислено/создано (для заголовков и SSE)."""
event: XpEvent | None
spawned: Task | None
coins: int # монеты за задачу, включая бонус за уровень
def handle_task_closed(db: Session, task: Task, via_options: bool = False) -> CloseOutcome:
"""Задача перешла в done (done_at уже поставлен) — начислить всё положенное.
Спавн следующего экземпляра регулярной — только если ещё не спавнили
(spawned_at is None); XP/монеты grant_task_xp не даст повторно и сам.
"""
outcome = CloseOutcome(event=None, spawned=None, coins=0)
if task.spawned_at is None:
spawned = spawn_next(db, task)
if spawned is not None:
task.spawned_at = utcnow()
outcome.spawned = spawned
event = xp_service.grant_task_xp(db, task)
if event is not None:
event.via_options = via_options
outcome.event = event
user_id = task.user_id or ""
# Монеты — вторая награда за закрытие (валюта сада, рядом с XP)
old_level = xp_service.level_from_xp(xp_service.total_xp(db, user_id) - event.amount)
coins = coins_for_xp(event.amount)
db.add(CoinEvent(user_id=user_id, task_id=task.id, source="task", amount=coins))
new_level = xp_service.level_from_xp(xp_service.total_xp(db, user_id))
if new_level > old_level:
coins += level_bonus_coins(new_level)
db.add(
CoinEvent(
user_id=user_id,
source="level",
level=new_level,
amount=level_bonus_coins(new_level),
)
)
outcome.coins = coins
return outcome